From 3393c73df22e867e43020733933ebb4385624f9c Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Sun, 22 Oct 2023 19:02:34 -0700 Subject: [PATCH] Add PNG importing of GameDataTextures --- .../Importing/Gtf/GtfDecoder.cs | 206 ++++++++++++++++++ Refresh.GameServer/Importing/Gtf/GtfFormat.cs | 15 ++ Refresh.GameServer/Importing/Gtf/GtfHeader.cs | 38 ++++ .../Importing/Gtf/GtfMetadata.cs | 6 + .../Importing/Gtf/GtfPixelFormat.cs | 77 +++++++ .../Importing/ImageImporter.Conversions.cs | 8 + Refresh.GameServer/Importing/ImageImporter.cs | 5 +- Refresh.GameServer/Importing/TexStream.cs | 15 +- Refresh.GameServer/Refresh.GameServer.csproj | 1 + 9 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 Refresh.GameServer/Importing/Gtf/GtfDecoder.cs create mode 100644 Refresh.GameServer/Importing/Gtf/GtfFormat.cs create mode 100644 Refresh.GameServer/Importing/Gtf/GtfHeader.cs create mode 100644 Refresh.GameServer/Importing/Gtf/GtfMetadata.cs create mode 100644 Refresh.GameServer/Importing/Gtf/GtfPixelFormat.cs diff --git a/Refresh.GameServer/Importing/Gtf/GtfDecoder.cs b/Refresh.GameServer/Importing/Gtf/GtfDecoder.cs new file mode 100644 index 00000000..78370db1 --- /dev/null +++ b/Refresh.GameServer/Importing/Gtf/GtfDecoder.cs @@ -0,0 +1,206 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using Microsoft.Toolkit.HighPerformance; +using Pfim; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; + +namespace Refresh.GameServer.Importing.Gtf; + +public class GtfDecoder : ImageDecoder +{ + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Span magic = stackalloc byte[4]; + + stream.ReadExactly(magic); + + //Check magic + if(!magic.SequenceEqual("GTF "u8)) ThrowInvalidImageContentException(); + + //Read the header in + GtfHeader header = GtfHeader.Read(stream); + + //If we dont know what format this is, throw an error + if(!Enum.IsDefined(header.PixelFormat)) ThrowInvalidImageContentException(); + + Image image = new(options.Configuration, header.Width, header.Height); + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + + this.ProcessPixels(stream, pixels, header); + + return image; + } + + private void ProcessPixels(Stream compressedStream, Buffer2D pixels, GtfHeader header) where TPixel : unmanaged, IPixel + { + TexStream decompressedStream = new(compressedStream, false); + BEBinaryReader reader = new(decompressedStream); + + if (header.PixelFormat is GtfPixelFormat.CompressedDxt1 or GtfPixelFormat.CompressedDxt23 or GtfPixelFormat.CompressedDxt45) + { + BcDecoder decoder = new(); + + CompressionFormat format = header.PixelFormat switch { + GtfPixelFormat.CompressedDxt1 => CompressionFormat.Bc1, + GtfPixelFormat.CompressedDxt23 => CompressionFormat.Bc2, + GtfPixelFormat.CompressedDxt45 => CompressionFormat.Bc3, + _ => throw new ArgumentOutOfRangeException(), + }; + + // im not the happiest with this massive allocation :/ + Span colors = decoder.DecodeRaw(decompressedStream, pixels.Width, pixels.Height, format).AsSpan(); + + for (int y = 0; y < pixels.Height; y++) + { + Span row = pixels.DangerousGetRowSpan(y); + if (typeof(TPixel) == typeof(Rgba32)) + { + Span rgba32Row = row.Cast(); + colors.Slice(y * header.Width, header.Width).CopyTo(rgba32Row); + } + else + { + for (int x = 0; x < pixels.Width; x++) + { +#if NET8_0_OR_GREATER +#error Please move this to use Unsafe.BitCast(); +#endif + + TPixel pixel = new(); + pixel.FromRgba32(Unsafe.As(ref colors[y * header.Width + x])); + row[x] = pixel; + } + } + } + } + else + { + for (int y = 0; y < pixels.Height; y++) + { + Span row = pixels.DangerousGetRowSpan(y); + + for (int x = 0; x < row.Length; x++) + { + TPixel pixel = new(); + switch (header.PixelFormat) + { + + case GtfPixelFormat.B8: + pixel.FromRgba32(new Rgba32(0, 0, reader.ReadByte(), 255)); + break; + case GtfPixelFormat.A1R5G5B5: { + ushort packed = reader.ReadUInt16(); + byte a = ShiftLeftShiftingInLsb((byte)((packed & 0b1000000000000000) >> 15), 7); + byte r = ShiftLeftShiftingInLsb((byte)((packed & 0b0111110000000000) >> 10), 3); + byte g = ShiftLeftShiftingInLsb((byte)((packed & 0b0000001111100000) >> 5), 3); + byte b = ShiftLeftShiftingInLsb((byte)(packed & 0b0000000000011111), 3); + pixel.FromRgba32(new Rgba32(r, g, b, a)); + break; + } + case GtfPixelFormat.A4R4G4B4: { + ushort packed = reader.ReadUInt16(); + byte a = ShiftLeftShiftingInLsb((byte)((packed & 0b1111000000000000) >> 12), 4); + byte r = ShiftLeftShiftingInLsb((byte)((packed & 0b0000111100000000) >> 8), 4); + byte g = ShiftLeftShiftingInLsb((byte)((packed & 0b0000000011110000) >> 4), 4); + byte b = ShiftLeftShiftingInLsb((byte)(packed & 0b0000000000001111), 4); + pixel.FromRgba32(new Rgba32(r, g, b, a)); + break; + } + case GtfPixelFormat.R5G6B5: { + ushort packed = reader.ReadUInt16(); + byte r = ShiftLeftShiftingInLsb((byte)((packed & 0b1111100000000000) >> 11), 3); + byte g = ShiftLeftShiftingInLsb((byte)((packed & 0b0000011111100000) >> 5), 2); + byte b = ShiftLeftShiftingInLsb((byte)(packed & 0b0000000000011111), 3); + pixel.FromRgba32(new Rgba32(r, g, b, 255)); + break; + } + case GtfPixelFormat.A8R8G8B8: { + byte a = reader.ReadByte(); + byte r = reader.ReadByte(); + byte b = reader.ReadByte(); + byte g = reader.ReadByte(); + pixel.FromRgba32(new Rgba32(r, g, b, a)); + break; + } + case GtfPixelFormat.G8B8: + pixel.FromRgba32(new Rgba32(0, reader.ReadByte(), reader.ReadByte(), 255)); + break; + case GtfPixelFormat.R6G5B5: { + ushort packed = reader.ReadUInt16(); + byte r = ShiftLeftShiftingInLsb((byte)((packed & 0b1111110000000000) >> 10), 2); + byte g = ShiftLeftShiftingInLsb((byte)((packed & 0b0000001111100000) >> 5), 3); + byte b = ShiftLeftShiftingInLsb((byte)(packed & 0b0000000000011111), 3); + pixel.FromRgba32(new Rgba32(r, g, b, 255)); + break; + } + case GtfPixelFormat.R5G5B5A1: { + ushort packed = reader.ReadUInt16(); + byte r = ShiftLeftShiftingInLsb((byte)((packed & 0b1111100000000000) >> 11), 3); + byte g = ShiftLeftShiftingInLsb((byte)((packed & 0b0000011111000000) >> 6), 3); + byte b = ShiftLeftShiftingInLsb((byte)((packed & 0b0000000000111110) >> 1), 3); + byte a = ShiftLeftShiftingInLsb((byte)(packed & 0b0000000000000001), 7); + pixel.FromRgba32(new Rgba32(r, g, b, a)); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + row[x] = pixel; + } + } + } + } + + /// + /// Shifts a number to the left, shifting the LSB in + /// + /// The number to shift + /// The amount of bits to shift + /// The shifted number + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static byte ShiftLeftShiftingInLsb(byte num, byte amt) + { + int mask = (1 << amt) - 1; + return (byte)((num << amt) | ((num & 1) * mask)); + } + + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + return this.Decode(options, stream, cancellationToken); + } + + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Span magic = stackalloc byte[4]; + + stream.ReadExactly(magic); + + //Check magic + if(!magic.SequenceEqual("GTF "u8)) ThrowInvalidImageContentException(); + + //Read the header in + GtfHeader header = GtfHeader.Read(stream); + + //If we dont know what format this is, throw an error + if(!Enum.IsDefined(header.PixelFormat)) ThrowInvalidImageContentException(); + + return new ImageInfo( + new PixelTypeInfo(header.PixelFormat.BitsPerPixel()), + new Size(header.Width, header.Height), + new ImageMetadata + { + HorizontalResolution = header.Width, + VerticalResolution = header.Height, + ResolutionUnits = PixelResolutionUnit.AspectRatio, + } + ); + } + + [DoesNotReturn] + private static void ThrowInvalidImageContentException() + => throw new InvalidImageContentException("The image is not a valid GTF image."); +} \ No newline at end of file diff --git a/Refresh.GameServer/Importing/Gtf/GtfFormat.cs b/Refresh.GameServer/Importing/Gtf/GtfFormat.cs new file mode 100644 index 00000000..07008e77 --- /dev/null +++ b/Refresh.GameServer/Importing/Gtf/GtfFormat.cs @@ -0,0 +1,15 @@ +using SixLabors.ImageSharp.Formats; + +namespace Refresh.GameServer.Importing.Gtf; + +public class GtfFormat : IImageFormat +{ + public GtfMetadata CreateDefaultFormatMetadata() => new(); + + public string Name => "GTF"; + public string DefaultMimeType => ""; + public IEnumerable MimeTypes => new string[] { }; + public IEnumerable FileExtensions => new[] { "GTF" }; + + public static IImageFormat Instance { get; } = new GtfFormat(); +} \ No newline at end of file diff --git a/Refresh.GameServer/Importing/Gtf/GtfHeader.cs b/Refresh.GameServer/Importing/Gtf/GtfHeader.cs new file mode 100644 index 00000000..d9e34eae --- /dev/null +++ b/Refresh.GameServer/Importing/Gtf/GtfHeader.cs @@ -0,0 +1,38 @@ +namespace Refresh.GameServer.Importing.Gtf; + +public struct GtfHeader +{ + public GtfPixelFormat PixelFormat; + public bool Mipmap; + public byte Dimension; + public bool Cubemap; + public uint Remap; + public ushort Width; + public ushort Height; + public ushort Depth; + public byte Location; + public uint Pitch; + public uint Offset; + + public static GtfHeader Read(Stream stream) + { + BEBinaryReader reader = new(stream); + + GtfHeader header = new(); + + header.PixelFormat = (GtfPixelFormat)reader.ReadByte(); + header.Mipmap = reader.ReadBoolean(); + header.Dimension = reader.ReadByte(); + header.Cubemap = reader.ReadBoolean(); + header.Remap = reader.ReadUInt32(); + header.Width = reader.ReadUInt16(); + header.Height = reader.ReadUInt16(); + header.Depth = reader.ReadUInt16(); + header.Location = reader.ReadByte(); + _ = reader.ReadByte(); //unused padding + header.Pitch = reader.ReadUInt32(); + header.Offset = reader.ReadUInt32(); + + return header; + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Importing/Gtf/GtfMetadata.cs b/Refresh.GameServer/Importing/Gtf/GtfMetadata.cs new file mode 100644 index 00000000..b711f0ee --- /dev/null +++ b/Refresh.GameServer/Importing/Gtf/GtfMetadata.cs @@ -0,0 +1,6 @@ +namespace Refresh.GameServer.Importing.Gtf; + +public class GtfMetadata +{ + +} \ No newline at end of file diff --git a/Refresh.GameServer/Importing/Gtf/GtfPixelFormat.cs b/Refresh.GameServer/Importing/Gtf/GtfPixelFormat.cs new file mode 100644 index 00000000..f6f2da2e --- /dev/null +++ b/Refresh.GameServer/Importing/Gtf/GtfPixelFormat.cs @@ -0,0 +1,77 @@ +namespace Refresh.GameServer.Importing.Gtf; + +//See https://github.com/RPCS3/rpcs3/blob/23cb67e0a10f7d7ecf39122fa697af8dcc30690e/rpcs3/Emu/RSX/gcm_enums.h#L135 +public enum GtfPixelFormat : byte +{ + //Color flag + B8 = 0x81, + A1R5G5B5 = 0x82, + A4R4G4B4 = 0x83, + R5G6B5 = 0x84, + A8R8G8B8 = 0x85, + CompressedDxt1 = 0x86, + CompressedDxt23 = 0x87, + CompressedDxt45 = 0x88, + G8B8 = 0x8B, + CompressedB8R8G8R8 = 0x8D, // NOTE: 0xAD in firmware + CompressedR8B8R8G8 = 0x8E, // NOTE: 0xAE in firmware + R6G5B5 = 0x8F, + Depth24D8 = 0x90, + Depth24D8Float = 0x91, + Depth16 = 0x92, + Depth16Float = 0x93, + X16 = 0x94, + Y16X16 = 0x95, + R5G5B5A1 = 0x97, + CompressedHiLo8 = 0x98, + CompressedHiLoS8 = 0x99, + W16Z16Y16X16Float = 0x9A, + W32Z32Y32X32Float = 0x9B, + X32Float = 0x9C, + D1R5G5B5 = 0x9D, + D8R8G8B8 = 0x9E, + Y16X16Float = 0x9F, + //Swizzle flag + SZ = 0x00, + LN = 0x20, + // Normalization Flag + NR = 0x00, + UN = 0x40, +} + +public static class GtfPixelFormatExtensions +{ + public static int BitsPerPixel(this GtfPixelFormat pixelFormat) + { + return pixelFormat switch { + GtfPixelFormat.B8 => 8, + GtfPixelFormat.A1R5G5B5 => 16, + GtfPixelFormat.A4R4G4B4 => 16, + GtfPixelFormat.R5G6B5 => 16, + GtfPixelFormat.A8R8G8B8 => 32, + GtfPixelFormat.CompressedDxt1 => 32, + GtfPixelFormat.CompressedDxt23 => 32, + GtfPixelFormat.CompressedDxt45 => 32, + GtfPixelFormat.G8B8 => 16, + GtfPixelFormat.CompressedB8R8G8R8 => 32, + GtfPixelFormat.CompressedR8B8R8G8 => 32, + GtfPixelFormat.R6G5B5 => 16, + GtfPixelFormat.Depth24D8 => 32, + GtfPixelFormat.Depth24D8Float => 32, + GtfPixelFormat.Depth16 => 16, + GtfPixelFormat.Depth16Float => 16, + GtfPixelFormat.X16 => 16, + GtfPixelFormat.Y16X16 => 32, + GtfPixelFormat.R5G5B5A1 => 16, + GtfPixelFormat.CompressedHiLo8 => 16, + GtfPixelFormat.CompressedHiLoS8 => 16, + GtfPixelFormat.W16Z16Y16X16Float => 64, + GtfPixelFormat.W32Z32Y32X32Float => 128, + GtfPixelFormat.X32Float => 32, + GtfPixelFormat.D1R5G5B5 => 16, + GtfPixelFormat.D8R8G8B8 => 32, + GtfPixelFormat.Y16X16Float => 32, + _ => throw new ArgumentOutOfRangeException(nameof(pixelFormat), pixelFormat, null) + }; + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Importing/ImageImporter.Conversions.cs b/Refresh.GameServer/Importing/ImageImporter.Conversions.cs index 2e667ac8..13af2ba5 100644 --- a/Refresh.GameServer/Importing/ImageImporter.Conversions.cs +++ b/Refresh.GameServer/Importing/ImageImporter.Conversions.cs @@ -1,5 +1,7 @@ using ICSharpCode.SharpZipLib.Zip.Compression; using Pfim; +using Refresh.GameServer.Importing.Gtf; +using SixLabors.ImageSharp.Formats; namespace Refresh.GameServer.Importing; @@ -26,6 +28,12 @@ private static void TextureToPng(Stream stream, Stream writeStream) DdsToPng(new TexStream(stream), writeStream); } + private static void GtfToPng(Stream stream, Stream writeStream) + { + using Image image = new GtfDecoder().Decode(new DecoderOptions(), stream); + image.SaveAsPng(writeStream); + } + private static readonly PfimConfig Config = new(); private static void DdsToPng(Stream stream, Stream writeStream) diff --git a/Refresh.GameServer/Importing/ImageImporter.cs b/Refresh.GameServer/Importing/ImageImporter.cs index e06c79d4..579da99a 100644 --- a/Refresh.GameServer/Importing/ImageImporter.cs +++ b/Refresh.GameServer/Importing/ImageImporter.cs @@ -19,7 +19,7 @@ public void ImportFromDataStore(GameDatabaseContext context, IDataStore dataStor List assets = new(); assets.AddRange(context.GetAssetsByType(GameAssetType.Texture)); - // TODO: assets.AddRange(context.GetAssetsByType(GameAssetType.GameDataTexture)); + assets.AddRange(context.GetAssetsByType(GameAssetType.GameDataTexture)); assets.AddRange(context.GetAssetsByType(GameAssetType.Jpeg)); assets.AddRange(context.GetAssetsByType(GameAssetType.Png)); @@ -73,6 +73,9 @@ public static void ImportAsset(GameAsset asset, IDataStore dataStore) switch (asset.AssetType) { + case GameAssetType.GameDataTexture: + GtfToPng(stream, writeStream); + break; case GameAssetType.Texture: TextureToPng(stream, writeStream); break; diff --git a/Refresh.GameServer/Importing/TexStream.cs b/Refresh.GameServer/Importing/TexStream.cs index 672b1eda..a2e9c9fd 100644 --- a/Refresh.GameServer/Importing/TexStream.cs +++ b/Refresh.GameServer/Importing/TexStream.cs @@ -31,16 +31,19 @@ public class TexStream : Stream private readonly Inflater _inflater; - public TexStream(Stream stream) { + public TexStream(Stream stream, bool checkMagic = true) { this._stream = stream; BEBinaryReader reader = new(stream); - - //Read the file magic - int magic = reader.ReadInt32(); - if (magic != TexMagic) + + if (checkMagic) { - throw new FormatException("Input stream is not in TEX format!"); + //Read the file magic + int magic = reader.ReadInt32(); + if (magic != TexMagic) + { + throw new FormatException("Input stream is not in TEX format!"); + } } //Unknown flag, seems to always be 0x0001 diff --git a/Refresh.GameServer/Refresh.GameServer.csproj b/Refresh.GameServer/Refresh.GameServer.csproj index c407d3ed..061ebc15 100644 --- a/Refresh.GameServer/Refresh.GameServer.csproj +++ b/Refresh.GameServer/Refresh.GameServer.csproj @@ -61,6 +61,7 @@ +