mirror of
https://github.com/AssetRipper/AssetRipper.TextureDecoder.git
synced 2026-11-13 04:26:30 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d57e3461ff | ||
|
|
1d2d19c9ef | ||
|
|
f3ba84589d | ||
|
|
7cee390b2e | ||
|
|
024ac9eb8c | ||
|
|
d47fe4dcfb | ||
|
|
7d028ce3f5 | ||
|
|
beb819daa4 | ||
|
|
c9d338f159 |
No files matched your search
@@ -14,9 +14,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4.0.1
|
||||
uses: actions/setup-dotnet@v4.3.1
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AssetRipper.Text.SourceGeneration" Version="1.1.0" />
|
||||
<PackageReference Include="AssetRipper.Text.SourceGeneration" Version="1.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AssetRipper.Text.SourceGeneration" Version="1.1.0" />
|
||||
<PackageReference Include="AssetRipper.Text.SourceGeneration" Version="1.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -5,14 +5,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DecompressDXT1Test()
|
||||
public void Decompress_BC1()
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt1");
|
||||
int totalBytesRead = 0;
|
||||
@@ -36,7 +36,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DecompressDXT3Test()
|
||||
public void Decompress_BC2()
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt3");
|
||||
int totalBytesRead = 0;
|
||||
@@ -51,7 +51,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DecompressDXT5Test()
|
||||
public void Decompress_BC3()
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt5");
|
||||
int totalBytesRead = 0;
|
||||
@@ -66,7 +66,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DecompressBC4Test()
|
||||
public void Decompress_BC4()
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.BcTestFiles + "test.bc4");
|
||||
int bytesRead = Bc4.Decompress(data, 512, 512, out _);
|
||||
@@ -74,7 +74,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DecompressBC5Test()
|
||||
public void Decompress_BC5()
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.BcTestFiles + "test.bc5");
|
||||
int bytesRead = Bc5.Decompress(data, 512, 512, out _);
|
||||
@@ -84,7 +84,7 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
[TestCase("test.bc6h_fast")]
|
||||
[TestCase("test.bc6h_normal")]
|
||||
[TestCase("test.bc6h_best")]
|
||||
public void DecompressBC6HTest(string fileName)
|
||||
public void Decompress_BC6H(string fileName)
|
||||
{
|
||||
AssertCorrectBC6HDecompression(TestFileFolders.BcTestFiles + fileName, 512, 512, false);
|
||||
}
|
||||
@@ -92,14 +92,85 @@ namespace AssetRipper.TextureDecoder.Tests
|
||||
[TestCase("test.bc7_fast")]
|
||||
[TestCase("test.bc7_normal")]
|
||||
[TestCase("test.bc7_best")]
|
||||
public void DecompressBC7Test(string fileName)
|
||||
public void Decompress_BC7(string fileName)
|
||||
{
|
||||
AssertCorrectBC7Decompression(TestFileFolders.BcTestFiles + fileName, 512, 512);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Not implemented")]
|
||||
public void BC7PartialBlockTest([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
public void PartialBlock_BC1([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt1").AsSpan()[..Bc1.BlockSize];
|
||||
int bytesRead = Bc1.Decompress(data, width, height, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC2([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt3").AsSpan()[..Bc2.BlockSize];
|
||||
int bytesRead = Bc2.Decompress(data, width, height, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC3([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.DxtTestFiles + "test.dxt5").AsSpan()[..Bc3.BlockSize];
|
||||
int bytesRead = Bc3.Decompress(data, width, height, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC4([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.BcTestFiles + "test.bc4").AsSpan()[..Bc4.BlockSize];
|
||||
int bytesRead = Bc4.Decompress(data, width, height, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC5([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.BcTestFiles + "test.bc5").AsSpan()[..Bc5.BlockSize];
|
||||
int bytesRead = Bc5.Decompress(data, width, height, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC6H([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
ReadOnlySpan<byte> data = File.ReadAllBytes(TestFileFolders.BcTestFiles + "test.bc6h_best").AsSpan()[..Bc6h.BlockSize];
|
||||
int bytesRead = Bc6h.Decompress(data, width, height, false, out byte[] decodedData);
|
||||
Assert.That(bytesRead, Is.EqualTo(data.Length));
|
||||
Assert.That(decodedData, Has.Length.EqualTo(width * height * Unsafe.SizeOf<ColorBGRA32>()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PartialBlock_BC7([Range(1, 4)] int width, [Range(1, 4)] int height)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace AssetRipper.TextureDecoder.Atc
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static T ReadAtOffset<T>(this ReadOnlySpan<byte> input, int offset) where T : unmanaged
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
return MemoryMarshal.Read<T>(input.Slice(offset));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,22 @@ public static class Bc1
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 8;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -18,13 +34,14 @@ public static class Bc1
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int outputOffset = ((i * width) + j) * Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
BcHelpers.DecompressBc1(input.Slice(inputOffset, BlockSize), output.Slice(outputOffset), width * 4);
|
||||
BcHelpers.DecompressBc1(input.Slice(inputOffset, BlockSize), buffer);
|
||||
BcHelpers.CopyBufferToOutput(buffer, output, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,22 @@ public static class Bc2
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 16;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -18,13 +34,14 @@ public static class Bc2
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int outputOffset = ((i * width) + j) * Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
BcHelpers.DecompressBc2(input.Slice(inputOffset), output.Slice(outputOffset), width * 4);
|
||||
BcHelpers.DecompressBc2(input.Slice(inputOffset), buffer);
|
||||
BcHelpers.CopyBufferToOutput(buffer, output, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,22 @@ public static class Bc3
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 16;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -18,13 +34,14 @@ public static class Bc3
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int outputOffset = ((i * width) + j) * Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
BcHelpers.DecompressBc3(input.Slice(inputOffset), output.Slice(outputOffset), width * 4);
|
||||
BcHelpers.DecompressBc3(input.Slice(inputOffset), buffer);
|
||||
BcHelpers.CopyBufferToOutput(buffer, output, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,22 @@ public static class Bc4
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 8;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorR<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -19,21 +35,22 @@ public static class Bc4
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
int bufferSize = width * height * Unsafe.SizeOf<ColorR<byte>>();
|
||||
byte[] bufferArray = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
Span<byte> buffer = new Span<byte>(bufferArray, 0, bufferSize);
|
||||
int naturalSize = width * height * PixelSize;
|
||||
byte[] rentedArray = ArrayPool<byte>.Shared.Rent(naturalSize);
|
||||
Span<byte> naturalPixels = new Span<byte>(rentedArray, 0, naturalSize);
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int bufferOffset = ((i * width) + j) * Unsafe.SizeOf<ColorR<byte>>();
|
||||
BcHelpers.DecompressBc4(input.Slice(inputOffset), buffer.Slice(bufferOffset), width);
|
||||
BcHelpers.DecompressBc4(input.Slice(inputOffset), buffer);
|
||||
BcHelpers.CopyBufferToOutput(buffer, naturalPixels, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
RgbConverter.Convert<ColorR<byte>, byte, ColorBGRA32, byte>(buffer, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(bufferArray);
|
||||
RgbConverter.Convert<ColorR<byte>, byte, ColorBGRA32, byte>(naturalPixels, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(rentedArray);
|
||||
return inputOffset;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,22 @@ public static class Bc5
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 16;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRG<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -19,21 +35,22 @@ public static class Bc5
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
int bufferSize = width * height * Unsafe.SizeOf<ColorRG<byte>>();
|
||||
byte[] bufferArray = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
Span<byte> buffer = new Span<byte>(bufferArray, 0, bufferSize);
|
||||
int naturalSize = width * height * PixelSize;
|
||||
byte[] rentedArray = ArrayPool<byte>.Shared.Rent(naturalSize);
|
||||
Span<byte> naturalPixels = new Span<byte>(rentedArray, 0, naturalSize);
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int bufferOffset = ((i * width) + j) * Unsafe.SizeOf<ColorRG<byte>>();
|
||||
BcHelpers.DecompressBc5(input.Slice(inputOffset), buffer.Slice(bufferOffset), width * 2);
|
||||
BcHelpers.DecompressBc5(input.Slice(inputOffset), buffer);
|
||||
BcHelpers.CopyBufferToOutput(buffer, naturalPixels, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
RgbConverter.Convert<ColorRG<byte>, byte, ColorBGRA32, byte>(buffer, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(bufferArray);
|
||||
RgbConverter.Convert<ColorRG<byte>, byte, ColorBGRA32, byte>(naturalPixels, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(rentedArray);
|
||||
return inputOffset;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ public static class Bc6h
|
||||
/// The size of an encoded block, in bytes.
|
||||
/// </summary>
|
||||
public const int BlockSize = 16;
|
||||
/// <summary>
|
||||
/// The width of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockWidth = 4;
|
||||
/// <summary>
|
||||
/// The height of a decoded block, in pixels.
|
||||
/// </summary>
|
||||
private const int BlockHeight = 4;
|
||||
/// <summary>
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRGB<Half>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, bool isSigned, out byte[] output)
|
||||
{
|
||||
@@ -19,25 +36,27 @@ public static class Bc6h
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, bool isSigned, Span<byte> output)
|
||||
{
|
||||
int bufferSize = width * height * Unsafe.SizeOf<ColorRGB<Half>>();
|
||||
byte[] bufferArray = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
Span<byte> buffer = new Span<byte>(bufferArray, 0, bufferSize);
|
||||
int naturalSize = width * height * PixelSize;
|
||||
byte[] rentedArray = ArrayPool<byte>.Shared.Rent(naturalSize);
|
||||
Span<byte> naturalPixels = new Span<byte>(rentedArray, 0, naturalSize);
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += 4)
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += 4)
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int outputOffset = ((i * width) + j) * Unsafe.SizeOf<ColorRGB<Half>>();
|
||||
BcHelpers.DecompressBc6h(
|
||||
input.Slice(inputOffset, BlockSize),
|
||||
buffer.Slice(outputOffset),
|
||||
width * 3,
|
||||
buffer,
|
||||
isSigned);
|
||||
|
||||
BcHelpers.CopyBufferToOutput(buffer, naturalPixels, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
RgbConverter.Convert<ColorRGB<Half>, Half, ColorBGRA32, byte>(buffer, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(bufferArray);
|
||||
RgbConverter.Convert<ColorRGB<Half>, Half, ColorBGRA32, byte>(naturalPixels, width, height, output);
|
||||
ArrayPool<byte>.Shared.Return(rentedArray);
|
||||
return inputOffset;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public static class Bc7
|
||||
/// The size of the natural pixel type.
|
||||
/// </summary>
|
||||
private static int PixelSize => Unsafe.SizeOf<ColorRGBA<byte>>();
|
||||
/// <summary>
|
||||
/// The size of a decoded block, in bytes.
|
||||
/// </summary>
|
||||
internal static int DecodedBlockSize => BlockWidth * BlockHeight * PixelSize;
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, out byte[] output)
|
||||
{
|
||||
@@ -30,16 +34,19 @@ public static class Bc7
|
||||
|
||||
public static int Decompress(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[DecodedBlockSize];
|
||||
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < height; i += BlockHeight)
|
||||
{
|
||||
for (int j = 0; j < width; j += BlockWidth)
|
||||
{
|
||||
int outputOffset = ((i * width) + j) * PixelSize;
|
||||
BcHelpers.DecompressBc7(
|
||||
input.Slice(inputOffset, BlockSize),
|
||||
output.Slice(outputOffset),
|
||||
width * 4);
|
||||
buffer);
|
||||
|
||||
BcHelpers.CopyBufferToOutput(buffer, output, width, height, j, i, BlockWidth, BlockHeight, PixelSize);
|
||||
|
||||
inputOffset += BlockSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,52 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace AssetRipper.TextureDecoder.Bc;
|
||||
|
||||
internal static class BcHelpers
|
||||
{
|
||||
public static void DecompressBc1(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc1(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc1.DecodedBlockSize);
|
||||
int destinationPitch = 4 * 4;
|
||||
ColorBlock(compressedBlock, decompressedBlock, destinationPitch, 0);
|
||||
}
|
||||
|
||||
public static void DecompressBc2(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc2(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc2.DecodedBlockSize);
|
||||
int destinationPitch = 4 * 4;
|
||||
ColorBlock(compressedBlock.Slice(8), decompressedBlock, destinationPitch, 1);
|
||||
SharpAlphaBlock(compressedBlock, decompressedBlock.Slice(3), destinationPitch);
|
||||
}
|
||||
|
||||
public static void DecompressBc3(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc3(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc3.DecodedBlockSize);
|
||||
int destinationPitch = 4 * 4;
|
||||
ColorBlock(compressedBlock.Slice(8), decompressedBlock, destinationPitch, 1);
|
||||
SmoothAlphaBlock(compressedBlock, decompressedBlock.Slice(3), destinationPitch, 4);
|
||||
}
|
||||
|
||||
public static void DecompressBc4(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc4(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc4.DecodedBlockSize);
|
||||
int destinationPitch = 4 * 1;
|
||||
SmoothAlphaBlock(compressedBlock, decompressedBlock, destinationPitch, 1);
|
||||
}
|
||||
|
||||
public static void DecompressBc5(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc5(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc5.DecodedBlockSize);
|
||||
int destinationPitch = 4 * 2;
|
||||
SmoothAlphaBlock(compressedBlock, decompressedBlock, destinationPitch, 2);
|
||||
SmoothAlphaBlock(compressedBlock.Slice(8), decompressedBlock.Slice(1), destinationPitch, 2);
|
||||
}
|
||||
|
||||
public static void DecompressBc6h(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch, bool isSigned)
|
||||
public static void DecompressBc6h(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, bool isSigned)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc6h.DecodedBlockSize);
|
||||
|
||||
BitStream bstream = new(compressedBlock);
|
||||
Span<uint> r = stackalloc uint[4]; // wxyz
|
||||
Span<uint> g = stackalloc uint[4];
|
||||
@@ -448,15 +461,7 @@ internal static class BcHelpers
|
||||
// Do not use these in your encoder. If the hardware is passed blocks
|
||||
// with one of these modes specified, the resulting decompressed block
|
||||
// must contain all zeroes in all channels except for the alpha channel.
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
for (int j = 0; j < 4; ++j)
|
||||
{
|
||||
Span<byte> pixel = decompressedBlock.Slice(decompressedOffset + (j * 3 * sizeof(ushort)), 3 * sizeof(ushort));
|
||||
pixel.Clear();
|
||||
}
|
||||
decompressedOffset += destinationPitch * sizeof(ushort);
|
||||
}
|
||||
decompressedBlock.Clear();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -535,7 +540,7 @@ internal static class BcHelpers
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(decompressedBlock.Slice(bOffset), bFinal);
|
||||
}
|
||||
|
||||
decompressedOffset += destinationPitch * sizeof(ushort);
|
||||
decompressedOffset += 4 * 3 * sizeof(ushort);
|
||||
}
|
||||
|
||||
static uint ReadBc6hModeBits(ref BitStream bstream)
|
||||
@@ -553,30 +558,20 @@ internal static class BcHelpers
|
||||
}
|
||||
}
|
||||
|
||||
public static void DecompressBc7(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock, int destinationPitch)
|
||||
public static void DecompressBc7(ReadOnlySpan<byte> compressedBlock, Span<byte> decompressedBlock)
|
||||
{
|
||||
Debug.Assert(decompressedBlock.Length == Bc7.DecodedBlockSize);
|
||||
|
||||
BitStream bstream = new(compressedBlock);
|
||||
Span2D<uint> endpoints = new(stackalloc uint[6 * 4], 6, 4);
|
||||
Span2D<int> indices = new(stackalloc int[4 * 4], 4, 4);
|
||||
|
||||
int decompressedOffset = 0;
|
||||
|
||||
int mode = ReadBc7Mode(ref bstream);
|
||||
|
||||
// unexpected mode, clear the block (transparent black)
|
||||
if (mode >= 8)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
for (int j = 0; j < 4; ++j)
|
||||
{
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 0] = 0;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 1] = 0;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 2] = 0;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 3] = 0;
|
||||
}
|
||||
decompressedOffset += destinationPitch;
|
||||
}
|
||||
decompressedBlock.Clear();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -814,13 +809,12 @@ internal static class BcHelpers
|
||||
break;
|
||||
}
|
||||
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 0] = (byte)r;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 1] = (byte)g;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 2] = (byte)b;
|
||||
decompressedBlock[decompressedOffset + (j * 4) + 3] = (byte)a;
|
||||
int offset = (i * 16) + (j * 4);
|
||||
decompressedBlock[offset + 0] = (byte)r;
|
||||
decompressedBlock[offset + 1] = (byte)g;
|
||||
decompressedBlock[offset + 2] = (byte)b;
|
||||
decompressedBlock[offset + 3] = (byte)a;
|
||||
}
|
||||
|
||||
decompressedOffset += destinationPitch;
|
||||
}
|
||||
|
||||
static int ReadBc7Mode(ref BitStream bstream)
|
||||
@@ -1096,4 +1090,21 @@ internal static class BcHelpers
|
||||
{
|
||||
(a, b) = (b, a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is for copying a decoded block to the output buffer. It handles the pitch and non-4x4 blocks (if necessary).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static void CopyBufferToOutput(Span<byte> buffer, Span<byte> output, int width, int height, int w, int h, int blockWidth, int blockHeight, int pixelSize)
|
||||
{
|
||||
int hEnd = Math.Min(h + blockHeight, height);
|
||||
int wEnd = Math.Min(w + blockWidth, width);
|
||||
for (int i = h; i < hEnd; i++)
|
||||
{
|
||||
int outputOffset = ((i * width) + w) * pixelSize;
|
||||
int outputBufferOffset = ((i - h) * blockWidth) * pixelSize;
|
||||
int copyLength = (wEnd - w) * pixelSize;
|
||||
buffer.Slice(outputBufferOffset, copyLength).CopyTo(output.Slice(outputOffset, copyLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ namespace AssetRipper.TextureDecoder.Dxt
|
||||
where TOutputChannelValue : unmanaged
|
||||
where TOutputColor : unmanaged, IColor<TOutputChannelValue>
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
return DecompressDXT1<TOutputColor, TOutputChannelValue>(input, width, height, MemoryMarshal.Cast<byte, TOutputColor>(output));
|
||||
}
|
||||
|
||||
@@ -185,6 +186,7 @@ namespace AssetRipper.TextureDecoder.Dxt
|
||||
where TOutputChannelValue : unmanaged
|
||||
where TOutputColor : unmanaged, IColor<TOutputChannelValue>
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
return DecompressDXT3<TOutputColor, TOutputChannelValue>(input, width, height, MemoryMarshal.Cast<byte, TOutputColor>(output));
|
||||
}
|
||||
|
||||
@@ -316,6 +318,7 @@ namespace AssetRipper.TextureDecoder.Dxt
|
||||
where TOutputChannelValue : unmanaged
|
||||
where TOutputColor : unmanaged, IColor<TOutputChannelValue>
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
return DecompressDXT5<TOutputColor, TOutputChannelValue>(input, width, height, MemoryMarshal.Cast<byte, TOutputColor>(output));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressETC(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 8;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -50,6 +51,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressETC2(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 8;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -90,6 +92,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressETC2A1(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 8;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -130,6 +133,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressETC2A8(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 16;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -171,6 +175,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressEACRUnsigned(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 8;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -215,6 +220,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressEACRSigned(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 8;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -259,6 +265,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressEACRGUnsigned(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int bcw = (width + 3) / 4;
|
||||
int bch = (height + 3) / 4;
|
||||
|
||||
@@ -304,6 +311,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
public static int DecompressEACRGSigned(ReadOnlySpan<byte> input, int width, int height, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int requiredLength = ((width + 3) / 4) * ((height + 3) / 4) * 16;
|
||||
if (input.Length < requiredLength)
|
||||
{
|
||||
@@ -741,6 +749,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
private static void DecodeEtc2a8Block(ReadOnlySpan<byte> input, Span<uint> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int @base = input[0];
|
||||
int data1 = input[1];
|
||||
int mul = data1 >> 4;
|
||||
@@ -761,6 +770,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
private static void DecodeEacUnsignedBlock(ReadOnlySpan<byte> input, Span<uint> output, int channel)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int @base = input[0];
|
||||
int data1 = input[1];
|
||||
int mul = data1 >> 4;
|
||||
@@ -778,6 +788,7 @@ namespace AssetRipper.TextureDecoder.Etc
|
||||
|
||||
private static void DecodeEacSignedBlock(ReadOnlySpan<byte> input, Span<uint> output, int channel)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int @base = 127 + unchecked((sbyte)input[0]);
|
||||
int data1 = input[1];
|
||||
int mul = data1 >> 4;
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace AssetRipper.TextureDecoder.Pvrtc
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static int DecompressPVRTC(ReadOnlySpan<byte> input, int xDim, int yDim, bool do2bitMode, Span<byte> output)
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
int xBlockSize = do2bitMode ? BlockX2bpp : BlockX4bpp;
|
||||
// for MBX don't allow the sizes to get too small
|
||||
int blockXDim = Math.Max(2, xDim / xBlockSize);
|
||||
|
||||
@@ -503,6 +503,7 @@ namespace AssetRipper.TextureDecoder.Rgb
|
||||
where TDestinationChannel : unmanaged
|
||||
where TDestinationColor : unmanaged, IColor<TDestinationChannel>
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
ReadOnlySpan<TSourceColor> sourceSpan = MemoryMarshal.Cast<byte, TSourceColor>(input).Slice(0, width * height);
|
||||
Span<TDestinationColor> destinationSpan = MemoryMarshal.Cast<byte, TDestinationColor>(output).Slice(0, width * height);
|
||||
Convert<TSourceColor, TSourceChannel, TDestinationColor, TDestinationChannel>(sourceSpan, destinationSpan);
|
||||
|
||||
@@ -19,5 +19,13 @@
|
||||
throw new InvalidOperationException($"Not enough space. {spaceRequired} is required, but only {spaceAvailable} is available");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ThrowIfNotLittleEndian()
|
||||
{
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ namespace AssetRipper.TextureDecoder.Yuy2
|
||||
where TOutputChannelValue : unmanaged
|
||||
where TOutputColor : unmanaged, IColor<TOutputChannelValue>
|
||||
{
|
||||
ThrowHelper.ThrowIfNotLittleEndian();
|
||||
return DecompressYUY2<TOutputColor, TOutputChannelValue>(input, width, height, MemoryMarshal.Cast<byte, TOutputColor>(output));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
|
||||
<Version>2.2.1</Version>
|
||||
<Version>2.3.0</Version>
|
||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||
<Copyright>Copyright © ds5678</Copyright>
|
||||
<Authors>ds5678</Authors>
|
||||
|
||||
Reference in new issue
Block a user