Compare commits

...
12 Commits
Author SHA1 Message Date
ds5678 bc4d0f5378 Bump version prefix to 2.6.0
Updated the VersionPrefix property in Directory.Build.props from 2.5.0 to 2.6.0 to prepare for the next project release.
2026-01-25 14:53:25 -08:00
ds5678 8f0f94695b More accurate conversion from uint to ushort and byte 2026-01-25 14:51:56 -08:00
ds5678 aacbfbd222 More efficient constant estimation
Some accuracy is sacrificed
2026-01-25 14:34:19 -08:00
ds5678 93fe6948b6 Better identification for perfect sets of integer constants 2026-01-25 03:02:31 -08:00
ds5678 0e6f3080be Conversion between linear and sRGB 2026-01-25 00:25:41 -08:00
ds5678 f8294eff8b Move old RgbConverter methods to the test project 2026-01-25 00:16:09 -08:00
ds5678 0b4d1dfc83 Delete benchmarks project 2026-01-25 00:12:41 -08:00
ds5678 f5c1aca0e1 Improve numeric conversion involving floating point numbers
32+ bit integers <-> floating point numbers are more precise
2026-01-24 23:34:44 -08:00
ds5678 d4d335d94b Update NuGet packages and copyright year range
Upgraded BenchmarkDotNet, Microsoft.NET.Test.Sdk, NUnit3TestAdapter, and NUnit.Analyzers to latest versions in project files. Updated Microsoft.SourceLink.GitHub and extended copyright to 2026.
2026-01-24 22:42:27 -08:00
ds5678 70775daca9 Bump to .NET 10 2026-01-24 22:40:28 -08:00
dependabot[bot] 3a94ea1a0f Bump actions/checkout from 5 to 6 (#102)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 22:38:16 -08:00
dependabot[bot] db875125d2 Bump actions/setup-dotnet from 5.0.0 to 5.1.0 (#104)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.0.0 to 5.1.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.0.0...v5.1.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 22:37:59 -08:00
20 changed files with 801 additions and 715 deletions

No files matched your search

+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5.0.0
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 9.0.x
- name: Restore dependencies
@@ -1,25 +0,0 @@
using AssetRipper.TextureDecoder.Rgb;
using AssetRipper.TextureDecoder.Rgb.Formats;
using BenchmarkDotNet.Attributes;
namespace AssetRipper.TextureDecoder.Benchmarks;
public class Argb32Decoding
{
private const int Width = 256;//ignoring smaller mips
private const int Height = 256;//ignoring smaller mips
private readonly byte[] inputData;
private readonly byte[] outputData;
public Argb32Decoding()
{
inputData = File.ReadAllBytes(PathConstants.PathToTestFiles + "Rgb/" + "test.argb32");
outputData = new byte[Width * Height * sizeof(uint)];
}
[Benchmark]
public int OriginalMethod() => RgbConverter.ARGB32ToBGRA32(inputData, Width, Height, outputData);
[Benchmark]
public int GenericMethod() => RgbConverter.Convert<ColorARGB<byte>, byte, ColorBGRA<byte>, byte>(inputData, Width, Height, outputData);
}
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AssetRipper.TextureDecoder\AssetRipper.TextureDecoder.csproj" />
</ItemGroup>
</Project>
@@ -1,7 +0,0 @@
namespace AssetRipper.TextureDecoder.Benchmarks;
internal static class PathConstants
{
internal const string PathToRepositoryRoot = "../../../../../../../../";
internal const string PathToTestFiles = PathToRepositoryRoot + "TestFiles/";
}
@@ -1,11 +0,0 @@
using BenchmarkDotNet.Running;
namespace AssetRipper.TextureDecoder.Benchmarks;
internal class Program
{
static void Main()
{
BenchmarkRunner.Run(typeof(Program).Assembly);
}
}
@@ -1,25 +0,0 @@
using AssetRipper.TextureDecoder.Rgb;
using AssetRipper.TextureDecoder.Rgb.Formats;
using BenchmarkDotNet.Attributes;
namespace AssetRipper.TextureDecoder.Benchmarks;
public class Rgb9e5Decoding
{
private const int Width = 256;//ignoring smaller mips
private const int Height = 256;//ignoring smaller mips
private readonly byte[] inputData;
private readonly byte[] outputData;
public Rgb9e5Decoding()
{
inputData = File.ReadAllBytes(PathConstants.PathToTestFiles + "Rgb/" + "test.rgb9e5float");
outputData = new byte[Width * Height * sizeof(uint)];
}
[Benchmark]
public int OriginalMethod() => RgbConverter.RGB9e5FloatToBGRA32(inputData, Width, Height, outputData);
[Benchmark]
public int GenericMethod() => RgbConverter.Convert<ColorRGB9e5, double, ColorBGRA<byte>, byte>(inputData, Width, Height, outputData);
}
@@ -163,10 +163,11 @@ internal static class NumericConversionGenerator
else
{
Debug.Assert(to.IsUnsignedInteger);
if (from.Type == typeof(Half))
CSharpPrimitives.Data conversionType = CSharpPrimitives.GetDataForIntegerFloatingPointConversion(to, from);
if (conversionType != from)
{
writer.WriteComment("We use float because it has enough precision to convert from Half to any integer type.");
writer.WriteLine($"return {ConvertMethodName(typeof(float))}<TTo>((float)value);");
writer.WriteComment($"We use {conversionType.LangName} because it has enough precision to convert from {from.LangName} to {to.LangName}.");
writer.WriteLine($"return {ConvertMethodName(conversionType.Type)}<TTo>(({conversionType.LangName})value);");
}
else
{
@@ -184,12 +185,13 @@ internal static class NumericConversionGenerator
Debug.Assert(from.IsUnsignedInteger);
if (to.IsFloatingPoint)
{
if (to.Type == typeof(Half))
CSharpPrimitives.Data conversionType = CSharpPrimitives.GetDataForIntegerFloatingPointConversion(from, to);
if (conversionType != to)
{
writer.WriteComment("There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.");
writer.WriteLine($"float x = {methodName}<float>(value);");
writer.WriteLine("Half converted = (Half)x;");
writer.WriteLine("return Unsafe.As<Half, TTo>(ref converted);");
writer.WriteComment($"There isn't enough precision to convert from {from.LangName} to {to.LangName}, so we convert to {conversionType.LangName} first.");
writer.WriteLine($"{conversionType.LangName} x = {methodName}<{conversionType.LangName}>(value);");
writer.WriteLine($"{to.LangName} converted = ({to.LangName})x;");
writer.WriteLine($"return Unsafe.As<{to.LangName}, TTo>(ref converted);");
}
else
{
@@ -227,6 +229,13 @@ internal static class NumericConversionGenerator
writer.WriteLine("byte converted = unchecked((byte)x);");
writer.WriteLine("return Unsafe.As<byte, TTo>(ref converted);");
}
else if (from.Size < sizeof(double) && to.Size < sizeof(double))
{
writer.WriteLine($"double interpolated = (double)value / (double){from.MaxValue};");
writer.WriteLine($"double exact = interpolated * (double){to.MaxValue};");
writer.WriteLine($"{to.LangName} converted = ({to.LangName})double.Round(exact, MidpointRounding.AwayFromZero);");
writer.WriteLine($"return Unsafe.As<{to.LangName}, TTo>(ref converted);");
}
else
{
writer.WriteComment($"There are more accurate ways to map {from.TypeName} onto {to.TypeName}, but this is the simplest.");
@@ -68,11 +68,18 @@ public static class CSharpPrimitives
return type == typeof(Half) || type == typeof(float) || type == typeof(NFloat) || type == typeof(double) || type == typeof(decimal);
}
public static bool IsInteger(Type type)
{
return IsUnsignedInteger(type) || IsSignedInteger(type);
}
public static bool IsUnsignedInteger(Type type)
{
return type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(nuint) || type == typeof(ulong) || type == typeof(UInt128);
}
public static bool IsSignedInteger(Type type) => IsSignedInteger(type, out _);
public static bool IsSignedInteger(Type type, [NotNullWhen(true)] out Type? unsignedType)
{
if (type == typeof(sbyte))
@@ -195,6 +202,21 @@ public static class CSharpPrimitives
}
}
public static Data GetDataForIntegerFloatingPointConversion(Data integerType, Data floatingPointType)
{
if (!IsInteger(integerType.Type))
{
throw new ArgumentException("The provided integer type is not an integer type.", nameof(integerType));
}
if (!IsFloatingPoint(floatingPointType.Type))
{
throw new ArgumentException("The provided floating point type is not a floating point type.", nameof(floatingPointType));
}
Data result = integerType.Size < sizeof(uint) ? Dictionary[typeof(float)] : Dictionary[typeof(double)];
return floatingPointType.Size >= result.Size ? floatingPointType : result;
}
public abstract class Data
{
public abstract Type Type { get; }
@@ -5,10 +5,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.10.0">
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -0,0 +1,512 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
namespace AssetRipper.TextureDecoder.Tests;
internal static class OldRgbConverter
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int A8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return A8ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int A8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = 0; // r
output[oo + 3] = input[io]; // a
oo += 4;
io++;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB24ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB24ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB24ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 2]; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 3;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBA32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 2]; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = input[io + 3]; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int ARGB32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return ARGB32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int ARGB32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 3]; // b
output[oo + 1] = input[io + 2]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = input[io + 0]; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB16ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = (byte)(input[io + 1] & 0xF8);
byte g = unchecked((byte)((input[io + 1] << 5) | ((input[io + 0] & 0xE0) >> 3)));
byte b = unchecked((byte)(input[io + 0] << 3));
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RG16ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int R8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return R8ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int R8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 1;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io + 0) * 255f);
byte g = ClampByte(ToHalf(input, io + 2) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBAHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io + 0) * 255f);
byte g = ClampByte(ToHalf(input, io + 2) * 255f);
byte b = ClampByte(ToHalf(input, io + 4) * 255f);
byte a = ClampByte(ToHalf(input, io + 6) * 255f);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = a; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io + 0) * 255f);
byte g = ClampByte(ToSingle(input, io + 4) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBAFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io + 0) * 255f);
byte g = ClampByte(ToSingle(input, io + 4) * 255f);
byte b = ClampByte(ToSingle(input, io + 8) * 255f);
byte a = ClampByte(ToSingle(input, io + 12) * 255f);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = a; // a
io += 16;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB9e5FloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB9e5FloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB9e5FloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
uint value = BinaryPrimitives.ReadUInt32LittleEndian(input.Slice(io, 4));
double scale = Math.Pow(2, unchecked((int)(value >> 27) - 24));
byte r = ClampByte((value >> 0 & 0x1FF) * scale * 255.0);
byte g = ClampByte((value >> 9 & 0x1FF) * scale * 255.0);
byte b = ClampByte((value >> 18 & 0x1FF) * scale * 255.0);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RG32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB48ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB48ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB48ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 5]; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = 255; // a
io += 6;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA64ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBA64ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA64ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 5]; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = input[io + 7]; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static float ToHalf(ReadOnlySpan<byte> input, int offset)
{
return (float)BinaryPrimitives.ReadHalfLittleEndian(input.Slice(offset, 2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static float ToSingle(ReadOnlySpan<byte> input, int offset)
{
return BinaryPrimitives.ReadSingleLittleEndian(input.Slice(offset, 4));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static byte ClampByte(float x)
{
return byte.MaxValue < x ? byte.MaxValue : (x > byte.MinValue ? (byte)x : byte.MinValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static byte ClampByte(double x)
{
return byte.MaxValue < x ? byte.MaxValue : (x > byte.MinValue ? (byte)x : byte.MinValue);
}
}
@@ -3,7 +3,7 @@ using AssetRipper.TextureDecoder.Rgb.Formats;
namespace AssetRipper.TextureDecoder.Tests
{
public sealed class NewRgbTests
public sealed class RgbConsistencyTests
{
[Test]
public void ConvertA8Test()
@@ -12,7 +12,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.A8ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.A8ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorA<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -40,7 +40,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.RGB24ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.RGB24ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGB<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -55,7 +55,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.RGBA32ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.RGBA32ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGBA<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -70,7 +70,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.ARGB32ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.ARGB32ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorARGB<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -85,7 +85,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.RGB16ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.RGB16ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGB16, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -126,7 +126,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.RG16ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.RG16ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRG<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -141,7 +141,7 @@ namespace AssetRipper.TextureDecoder.Tests
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = RgbConverter.R8ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
int bytesRead = OldRgbConverter.R8ToBGRA32(data.Slice(totalBytesRead), size, size, out byte[] originalDecodedData);
RgbConverter.Convert<ColorR<byte>, byte, ColorBGRA<byte>, byte>(data.Slice(totalBytesRead), size, size, out byte[] newDecodedData);
totalBytesRead += bytesRead;
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
@@ -153,7 +153,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rhalf");
RgbConverter.RHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorR<Half>, Half, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -162,7 +162,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rghalf");
RgbConverter.RGHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RGHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRG<Half>, Half, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -171,7 +171,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGBAHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgbahalf");
RgbConverter.RGBAHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RGBAHalfToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGBA<Half>, Half, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -180,7 +180,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rfloat");
RgbConverter.RFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorR<float>, float, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -189,7 +189,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgfloat");
RgbConverter.RGFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RGFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRG<float>, float, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -198,7 +198,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGBAFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgbafloat");
RgbConverter.RGBAFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RGBAFloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGBA<float>, float, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -207,7 +207,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGB9e5FloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgb9e5float");
RgbConverter.RGB9e5FloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
OldRgbConverter.RGB9e5FloatToBGRA32(data, 256, 256, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGB9e5, double, ColorBGRA<byte>, byte>(data, 256, 256, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -216,7 +216,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRG32Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rg32");
RgbConverter.RG32ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
OldRgbConverter.RG32ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRG<ushort>, ushort, ColorBGRA<byte>, byte>(data, 512, 512, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -225,7 +225,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGB48Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgb48");
RgbConverter.RGB48ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
OldRgbConverter.RGB48ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGB<ushort>, ushort, ColorBGRA<byte>, byte>(data, 512, 512, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
@@ -234,7 +234,7 @@ namespace AssetRipper.TextureDecoder.Tests
public void ConvertRGBA64Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgba64");
RgbConverter.RGBA64ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
OldRgbConverter.RGBA64ToBGRA32(data, 512, 512, out byte[] originalDecodedData);
RgbConverter.Convert<ColorRGBA<ushort>, ushort, ColorBGRA<byte>, byte>(data, 512, 512, out byte[] newDecodedData);
Assert.That(originalDecodedData, Is.EqualTo(newDecodedData));
}
+28 -18
View File
@@ -1,4 +1,7 @@
namespace AssetRipper.TextureDecoder.Tests
using AssetRipper.TextureDecoder.Rgb;
using AssetRipper.TextureDecoder.Rgb.Formats;
namespace AssetRipper.TextureDecoder.Tests
{
public sealed class RgbTests
{
@@ -9,7 +12,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.A8ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorA<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -22,7 +25,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.RGB24ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorRGB<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -35,7 +38,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.RGBA32ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorRGBA<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -48,7 +51,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.ARGB32ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorARGB<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -61,7 +64,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.RGB16ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorRGB16, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -74,7 +77,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.RG16ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorRG<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -87,7 +90,7 @@
int totalBytesRead = 0;
foreach (int size in new int[] { 256, 128, 64, 32, 16, 8, 4, 2, 1 }) //mip maps
{
int bytesRead = Rgb.RgbConverter.R8ToBGRA32(data.Slice(totalBytesRead), size, size, out _);
int bytesRead = ConvertSquare<ColorR<byte>, byte>(data.Slice(totalBytesRead), size);
totalBytesRead += bytesRead;
}
Assert.That(totalBytesRead, Is.EqualTo(data.Length));
@@ -97,7 +100,7 @@
public void ConvertRHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rhalf");
int bytesRead = Rgb.RgbConverter.RHalfToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorR<Half>, Half>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -105,7 +108,7 @@
public void ConvertRGHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rghalf");
int bytesRead = Rgb.RgbConverter.RGHalfToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorRG<Half>, Half>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -113,7 +116,7 @@
public void ConvertRGBAHalfTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgbahalf");
int bytesRead = Rgb.RgbConverter.RGBAHalfToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorRGBA<Half>, Half>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -121,7 +124,7 @@
public void ConvertRFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rfloat");
int bytesRead = Rgb.RgbConverter.RFloatToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorR<float>, float>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -129,7 +132,7 @@
public void ConvertRGFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgfloat");
int bytesRead = Rgb.RgbConverter.RGFloatToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorRG<float>, float>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -137,7 +140,7 @@
public void ConvertRGBAFloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgbafloat");
int bytesRead = Rgb.RgbConverter.RGBAFloatToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorRGBA<float>, float>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -145,7 +148,7 @@
public void ConvertRGB9e5FloatTest()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgb9e5float");
int bytesRead = Rgb.RgbConverter.RGB9e5FloatToBGRA32(data, 256, 256, out _);
int bytesRead = ConvertSquare<ColorRGB9e5, double>(data, 256);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -153,7 +156,7 @@
public void ConvertRG32Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rg32");
int bytesRead = Rgb.RgbConverter.RG32ToBGRA32(data, 512, 512, out _);
int bytesRead = ConvertSquare<ColorRG<ushort>, ushort>(data, 512);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -161,7 +164,7 @@
public void ConvertRGB48Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgb48");
int bytesRead = Rgb.RgbConverter.RGB48ToBGRA32(data, 512, 512, out _);
int bytesRead = ConvertSquare<ColorRGB<ushort>, ushort>(data, 512);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
@@ -169,8 +172,15 @@
public void ConvertRGBA64Test()
{
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.RgbTestFiles + "test.rgba64");
int bytesRead = Rgb.RgbConverter.RGBA64ToBGRA32(data, 512, 512, out _);
int bytesRead = ConvertSquare<ColorRGBA<ushort>, ushort>(data, 512);
Assert.That(bytesRead, Is.EqualTo(data.Length));
}
private static int ConvertSquare<TColor, TChannel>(ReadOnlySpan<byte> data, int size)
where TColor : unmanaged, IColor<TChannel>
where TChannel : unmanaged
{
return RgbConverter.Convert<TColor, TChannel, ColorBGRA<byte>, byte>(data, size, size, out _);
}
}
}
@@ -10,7 +10,7 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/AssetRipper/TextureDecoder</PackageProjectUrl>
<Copyright>Copyright (c) 2022 - 2024 ds5678</Copyright>
<Copyright>Copyright (c) 2022 - 2026 ds5678</Copyright>
<Description>Managed library for decoding textures in multiple formats.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@@ -20,7 +20,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+26
View File
@@ -92,4 +92,30 @@ public static class Color
return destination;
}
}
internal static TColor SRgbToLinear<TColor, TChannel>(this TColor color)
where TChannel : unmanaged
where TColor : unmanaged, IColor<TChannel>
{
color.GetChannels(out TChannel r, out TChannel g, out TChannel b, out TChannel a);
color.SetChannels(
SRgb.ToLinear(r),
SRgb.ToLinear(g),
SRgb.ToLinear(b),
a);
return color;
}
internal static TColor LinearToSRgb<TColor, TChannel>(this TColor color)
where TChannel : unmanaged
where TColor : unmanaged, IColor<TChannel>
{
color.GetChannels(out TChannel r, out TChannel g, out TChannel b, out TChannel a);
color.SetChannels(
SRgb.FromLinear(r),
SRgb.FromLinear(g),
SRgb.FromLinear(b),
a);
return color;
}
}
@@ -187,7 +187,7 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(Half))
{
// There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.
// There isn't enough precision to convert from byte to Half, so we convert to float first.
float x = ConvertByte<float>(value);
Half converted = (Half)x;
return Unsafe.As<Half, TTo>(ref converted);
@@ -327,7 +327,7 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(Half))
{
// There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.
// There isn't enough precision to convert from ushort to Half, so we convert to float first.
float x = ConvertUInt16<float>(value);
Half converted = (Half)x;
return Unsafe.As<Half, TTo>(ref converted);
@@ -383,12 +383,10 @@ static partial class NumericConversion
else if (typeof(TTo) == typeof(byte))
{
// See https://github.com/AssetRipper/TextureDecoder/issues/19
// There are more accurate ways to map UInt32 onto Byte, but this is the simplest.
unchecked
{
byte converted = (byte)((uint)value >> 24);
return Unsafe.As<byte, TTo>(ref converted);
}
double interpolated = (double)value / (double)uint.MaxValue;
double exact = interpolated * (double)byte.MaxValue;
byte converted = (byte)double.Round(exact, MidpointRounding.AwayFromZero);
return Unsafe.As<byte, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(short))
{
@@ -398,12 +396,10 @@ static partial class NumericConversion
else if (typeof(TTo) == typeof(ushort))
{
// See https://github.com/AssetRipper/TextureDecoder/issues/19
// There are more accurate ways to map UInt32 onto UInt16, but this is the simplest.
unchecked
{
ushort converted = (ushort)((uint)value >> 16);
return Unsafe.As<ushort, TTo>(ref converted);
}
double interpolated = (double)value / (double)uint.MaxValue;
double exact = interpolated * (double)ushort.MaxValue;
ushort converted = (ushort)double.Round(exact, MidpointRounding.AwayFromZero);
return Unsafe.As<ushort, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(int))
{
@@ -470,14 +466,16 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(Half))
{
// There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.
float x = ConvertUInt32<float>(value);
// There isn't enough precision to convert from uint to Half, so we convert to double first.
double x = ConvertUInt32<double>(value);
Half converted = (Half)x;
return Unsafe.As<Half, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(float))
{
float converted = (float)value / (float)uint.MaxValue;
// There isn't enough precision to convert from uint to float, so we convert to double first.
double x = ConvertUInt32<double>(value);
float converted = (float)x;
return Unsafe.As<float, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(NFloat))
@@ -640,14 +638,16 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(Half))
{
// There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.
float x = ConvertUInt64<float>(value);
// There isn't enough precision to convert from ulong to Half, so we convert to double first.
double x = ConvertUInt64<double>(value);
Half converted = (Half)x;
return Unsafe.As<Half, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(float))
{
float converted = (float)value / (float)ulong.MaxValue;
// There isn't enough precision to convert from ulong to float, so we convert to double first.
double x = ConvertUInt64<double>(value);
float converted = (float)x;
return Unsafe.As<float, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(NFloat))
@@ -785,14 +785,16 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(Half))
{
// There isn't enough precision to convert from anything bigger than byte to Half, so we convert to float first.
float x = ConvertUInt128<float>(value);
// There isn't enough precision to convert from UInt128 to Half, so we convert to double first.
double x = ConvertUInt128<double>(value);
Half converted = (Half)x;
return Unsafe.As<Half, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(float))
{
float converted = (float)value / (float)UInt128.MaxValue;
// There isn't enough precision to convert from UInt128 to float, so we convert to double first.
double x = ConvertUInt128<double>(value);
float converted = (float)x;
return Unsafe.As<float, TTo>(ref converted);
}
else if (typeof(TTo) == typeof(NFloat))
@@ -834,7 +836,7 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(byte))
{
// We use float because it has enough precision to convert from Half to any integer type.
// We use float because it has enough precision to convert from Half to byte.
return ConvertSingle<TTo>((float)value);
}
else if (typeof(TTo) == typeof(short))
@@ -844,7 +846,7 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(ushort))
{
// We use float because it has enough precision to convert from Half to any integer type.
// We use float because it has enough precision to convert from Half to ushort.
return ConvertSingle<TTo>((float)value);
}
else if (typeof(TTo) == typeof(int))
@@ -854,8 +856,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(uint))
{
// We use float because it has enough precision to convert from Half to any integer type.
return ConvertSingle<TTo>((float)value);
// We use double because it has enough precision to convert from Half to uint.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(nint))
{
@@ -890,8 +892,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(ulong))
{
// We use float because it has enough precision to convert from Half to any integer type.
return ConvertSingle<TTo>((float)value);
// We use double because it has enough precision to convert from Half to ulong.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(Int128))
{
@@ -900,8 +902,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(UInt128))
{
// We use float because it has enough precision to convert from Half to any integer type.
return ConvertSingle<TTo>((float)value);
// We use double because it has enough precision to convert from Half to UInt128.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(Half))
{
@@ -975,10 +977,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(uint))
{
// x must be clamped because of rounding errors.
float x = value * (float)uint.MaxValue;
uint converted = (float)uint.MaxValue < x ? uint.MaxValue : (x > (float)uint.MinValue ? (uint)x : uint.MinValue);
return Unsafe.As<uint, TTo>(ref converted);
// We use double because it has enough precision to convert from float to uint.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(nint))
{
@@ -1013,10 +1013,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(ulong))
{
// x must be clamped because of rounding errors.
float x = value * (float)ulong.MaxValue;
ulong converted = (float)ulong.MaxValue < x ? ulong.MaxValue : (x > (float)ulong.MinValue ? (ulong)x : ulong.MinValue);
return Unsafe.As<ulong, TTo>(ref converted);
// We use double because it has enough precision to convert from float to ulong.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(Int128))
{
@@ -1025,10 +1023,8 @@ static partial class NumericConversion
}
else if (typeof(TTo) == typeof(UInt128))
{
// x must be clamped because of rounding errors.
float x = value * (float)UInt128.MaxValue;
UInt128 converted = (float)UInt128.MaxValue < x ? UInt128.MaxValue : (x > (float)UInt128.MinValue ? (UInt128)x : UInt128.MinValue);
return Unsafe.As<UInt128, TTo>(ref converted);
// We use double because it has enough precision to convert from float to UInt128.
return ConvertDouble<TTo>((double)value);
}
else if (typeof(TTo) == typeof(Half))
{
+14 -501
View File
@@ -1,490 +1,7 @@
using System.Buffers.Binary;
namespace AssetRipper.TextureDecoder.Rgb
{
public static class RgbConverter
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int A8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return A8ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int A8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = 0; // r
output[oo + 3] = input[io]; // a
oo += 4;
io++;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB24ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB24ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB24ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 2]; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 3;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBA32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 2]; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = input[io + 3]; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int ARGB32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return ARGB32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int ARGB32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 3]; // b
output[oo + 1] = input[io + 2]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = input[io + 0]; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB16ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = (byte)(input[io + 1] & 0xF8);
byte g = unchecked((byte)((input[io + 1] << 5) | ((input[io + 0] & 0xE0) >> 3)));
byte b = unchecked((byte)(input[io + 0] << 3));
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RG16ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG16ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = input[io + 1]; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int R8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return R8ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int R8ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = input[io + 0]; // r
output[oo + 3] = 255; // a
io += 1;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 2;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io + 0) * 255f);
byte g = ClampByte(ToHalf(input, io + 2) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBAHalfToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAHalfToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToHalf(input, io + 0) * 255f);
byte g = ClampByte(ToHalf(input, io + 2) * 255f);
byte b = ClampByte(ToHalf(input, io + 4) * 255f);
byte a = ClampByte(ToHalf(input, io + 6) * 255f);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = a; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = 0; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io + 0) * 255f);
byte g = ClampByte(ToSingle(input, io + 4) * 255f);
output[oo + 0] = 0; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBAFloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBAFloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
byte r = ClampByte(ToSingle(input, io + 0) * 255f);
byte g = ClampByte(ToSingle(input, io + 4) * 255f);
byte b = ClampByte(ToSingle(input, io + 8) * 255f);
byte a = ClampByte(ToSingle(input, io + 12) * 255f);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = a; // a
io += 16;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB9e5FloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB9e5FloatToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB9e5FloatToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
uint value = BinaryPrimitives.ReadUInt32LittleEndian(input.Slice(io, 4));
double scale = Math.Pow(2, unchecked((int)(value >> 27) - 24));
byte r = ClampByte((value >> 0 & 0x1FF) * scale * 255.0);
byte g = ClampByte((value >> 9 & 0x1FF) * scale * 255.0);
byte b = ClampByte((value >> 18 & 0x1FF) * scale * 255.0);
output[oo + 0] = b; // b
output[oo + 1] = g; // g
output[oo + 2] = r; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RG32ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RG32ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = 0; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = 255; // a
io += 4;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB48ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGB48ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGB48ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 5]; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = 255; // a
io += 6;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA64ToBGRA32(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
{
output = new byte[width * height * 4];
return RGBA64ToBGRA32(input, width, height, output);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int RGBA64ToBGRA32(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
{
int io = 0;
int oo = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
output[oo + 0] = input[io + 5]; // b
output[oo + 1] = input[io + 3]; // g
output[oo + 2] = input[io + 1]; // r
output[oo + 3] = input[io + 7]; // a
io += 8;
oo += 4;
}
}
return io;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static int Convert<TSourceColor, TSourceChannel, TDestinationColor, TDestinationChannel>(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
where TSourceChannel : unmanaged
@@ -548,28 +65,24 @@ namespace AssetRipper.TextureDecoder.Rgb
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static float ToHalf(ReadOnlySpan<byte> input, int offset)
public static void SRgbToLinear<TColor, TChannel>(Span<TColor> span)
where TChannel : unmanaged
where TColor : unmanaged, IColor<TChannel>
{
return (float)BinaryPrimitives.ReadHalfLittleEndian(input.Slice(offset, 2));
for (int i = 0; i < span.Length; i++)
{
span[i] = span[i].SRgbToLinear<TColor, TChannel>();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static float ToSingle(ReadOnlySpan<byte> input, int offset)
public static void LinearToSRgb<TColor, TChannel>(Span<TColor> span)
where TChannel : unmanaged
where TColor : unmanaged, IColor<TChannel>
{
return BinaryPrimitives.ReadSingleLittleEndian(input.Slice(offset, 4));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static byte ClampByte(float x)
{
return byte.MaxValue < x ? byte.MaxValue : (x > byte.MinValue ? (byte)x : byte.MinValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static byte ClampByte(double x)
{
return byte.MaxValue < x ? byte.MaxValue : (x > byte.MinValue ? (byte)x : byte.MinValue);
for (int i = 0; i < span.Length; i++)
{
span[i] = span[i].LinearToSRgb<TColor, TChannel>();
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
namespace AssetRipper.TextureDecoder.Rgb;
internal static class SRgb
{
public static T ToLinear<T>(T value) where T : unmanaged
{
double d = NumericConversion.Convert<T, double>(value);
d = SRgbToLinear(d);
return NumericConversion.Convert<double, T>(d);
}
public static T FromLinear<T>(T value) where T : unmanaged
{
double d = NumericConversion.Convert<T, double>(value);
d = LinearToSRgb(d);
return NumericConversion.Convert<double, T>(d);
}
private static double SRgbToLinear(double value)
{
if (value <= 0.04045)
{
return value / 12.92;
}
else
{
return double.Pow((value + 0.055) / 1.055, 2.4);
}
}
private static double LinearToSRgb(double value)
{
if (value <= 0.0031308)
{
return value * 12.92;
}
else
{
return 1.055 * double.Pow(value, 1.0 / 2.4) - 0.055;
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<VersionPrefix>2.5.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<Copyright>Copyright © ds5678</Copyright>
<Authors>ds5678</Authors>
<Company>AssetRipper</Company>
+74 -34
View File
@@ -8,17 +8,18 @@ internal class Program
{
int originalBitCount = int.Parse(args[0]);
int newBitCount = int.Parse(args[1]);
EstimateConstants(originalBitCount, newBitCount, out uint a, out uint b, out int c);
bool perfectMatch = EstimateConstants(originalBitCount, newBitCount, out uint a, out long b, out int c);
Console.WriteLine("Done!");
Console.WriteLine($"Result: ({a} * x + {b}) >> {c}");
Console.WriteLine($"Perfect match: {perfectMatch}");
}
private static void EstimateConstants(int originalBitCount, int newBitCount, out uint a, out uint b, out int c)
private static bool EstimateConstants(int originalBitCount, int newBitCount, out uint a, out long b, out int c)
{
a = default;
b = default;
c = default;
float bestError = float.MaxValue;//Sum of squared errors
double bestError = double.MaxValue;//Sum of squared errors
uint originalMaxValue = GetIntegerMaxValue(originalBitCount);
uint newMaxValue = GetIntegerMaxValue(newBitCount);
int bitDiff = newBitCount - originalBitCount;
@@ -27,55 +28,94 @@ internal class Program
//If bitDiff is negative, it means we are going from large numbers to small numbers.
//In other words, we will always have to shift right by at least enough to overcome the bit surplus.
int minc = int.Max(0, -bitDiff);
// We are limited to a 32-bit result, so we cannot shift left so much that we exceed 32 bits.
int maxc = 32 - newBitCount;
for (int ic = minc; ic <= maxc; ic++)
{
Console.WriteLine(ic);
//a: 2 ^ (c + bitDiff) to 2 ^ (c + bitDiff + 1)
uint mina = Pow2(ic + bitDiff);
uint maxa = Pow2(ic + bitDiff + 1);
//a: 2^c * newMaxValue / originalMaxValue rounded to nearest integer
double ia_exact = (double)Pow2(ic) * newMaxValue / originalMaxValue;
//b: 0 to 2^c - 1
//Any bigger and it will cause 0 not to correspond with 0.
uint minb = 0;
uint maxb = Pow2(ic) - 1;
ReadOnlySpan<uint> ia_values = ia_exact < 1 ? [(uint)double.Ceiling(ia_exact)] : [(uint)double.Floor(ia_exact), (uint)double.Ceiling(ia_exact)];
for (uint ia = mina; ia <= maxa; ia++)
foreach (uint ia in ia_values)
{
if (ConvertEstimate(originalMaxValue, ia, minb, ic) > newMaxValue || ConvertEstimate(originalMaxValue, ia, maxb, ic) < newMaxValue)
//b: 0 to 2^c - 1
//Any bigger and it will cause 0 not to correspond with 0.
uint minb;
uint maxb;
{
continue;
//We can restrict b further by ensuring that originalMaxValue maps to newMaxValue.
long minb_one = (long)(newMaxValue << ic) - (ia * originalMaxValue);
long maxb_one = minb_one + Pow2(ic) - 1;
minb = (uint)long.Max(0, minb_one);
maxb = (uint)long.Min(Pow2(ic) - 1, long.Max(0, maxb_one));
minb = uint.Min(minb, maxb);
}
for (uint ib = minb; ib <= maxb; ib++)
//Errors for b are nearly parabolic, so we try to find the minimum by ternary search.
//This is not guaranteed to find the absolute minimum since there's noise in the error, but it should get close enough.
uint current_b_low = minb;
uint current_b_high = maxb;
while (current_b_high - current_b_low > 3)
{
if (ConvertEstimate(originalMaxValue, ia, ib, ic) == newMaxValue)
uint range = current_b_high - current_b_low;
uint b1 = current_b_low + range / 3;
uint b2 = current_b_high - range / 3;
double error1 = CalculateMeanSquaredError(ia, b1, ic, originalMaxValue, newMaxValue, out _);
double error2 = CalculateMeanSquaredError(ia, b2, ic, originalMaxValue, newMaxValue, out _);
if (error1 < error2)
{
float error = 0;
for (uint x = 1; x < originalMaxValue; x++)
{
uint y = ConvertEstimate(x, ia, ib, ic);
float yExact = (float)x * newMaxValue / originalMaxValue;
float diff = yExact - y;
error += diff * diff;
}
if (error < bestError)
{
a = ia;
b = ib;
c = ic;
if (error == 0)//Huge performance savings for when newBitCount / originalBitCount has no remainder.
{
return;
}
bestError = error;
}
current_b_high = b2;
}
else
{
current_b_low = b1;
}
}
for (uint ib = current_b_low; ib <= current_b_high; ib++)
{
double error = CalculateMeanSquaredError(ia, ib, ic, originalMaxValue, newMaxValue, out bool anyIncorrect);
if (error < bestError)
{
bestError = error;
a = ia;
b = ib;
c = ic;
Console.WriteLine($"New best: a={a}, b={b}, c={c}, MSE={bestError}");
}
if (!anyIncorrect)
{
//Perfect match
return true;
}
}
}
}
return false;
}
private static double CalculateMeanSquaredError(uint a, uint b, int c, uint originalMaxValue, uint newMaxValue, out bool anyIncorrect)
{
double meanSquaredError = 0;
anyIncorrect = false;
for (uint x = 0; x <= originalMaxValue; x++)
{
uint y = ConvertEstimate(x, a, b, c);
double yExact = (double)x * newMaxValue / originalMaxValue;
double diff = yExact - y;
meanSquaredError = meanSquaredError * x / (x + 1) + (diff * diff) / (x + 1);
uint yExactInt = (uint)double.Round(yExact, MidpointRounding.AwayFromZero);
anyIncorrect |= (y != yExactInt);
}
return meanSquaredError;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
-1
View File
@@ -13,7 +13,6 @@
<Folder Name="/Solution Items/Workflows/">
<File Path=".github/workflows/build.yml" />
</Folder>
<Project Path="AssetRipper.TextureDecoder.Benchmarks/AssetRipper.TextureDecoder.Benchmarks.csproj" />
<Project Path="AssetRipper.TextureDecoder.ColorGenerator/AssetRipper.TextureDecoder.ColorGenerator.csproj" />
<Project Path="AssetRipper.TextureDecoder.ConsoleApp/AssetRipper.TextureDecoder.ConsoleApp.csproj" />
<Project Path="AssetRipper.TextureDecoder.Paths/AssetRipper.TextureDecoder.Paths.csproj" />