Skip to content

Commit

Permalink
Add PNG importing of GameDataTextures (#212)
Browse files Browse the repository at this point in the history
  • Loading branch information
jvyden authored Oct 23, 2023
2 parents e4252b3 + 3393c73 commit 8215e16
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 7 deletions.
206 changes: 206 additions & 0 deletions Refresh.GameServer/Importing/Gtf/GtfDecoder.cs
Original file line number Diff line number Diff line change
@@ -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<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Span<byte> 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<TPixel> image = new(options.Configuration, header.Width, header.Height);
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;

this.ProcessPixels(stream, pixels, header);

return image;
}

private void ProcessPixels<TPixel>(Stream compressedStream, Buffer2D<TPixel> pixels, GtfHeader header) where TPixel : unmanaged, IPixel<TPixel>
{
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<ColorRgba32> colors = decoder.DecodeRaw(decompressedStream, pixels.Width, pixels.Height, format).AsSpan();

for (int y = 0; y < pixels.Height; y++)
{
Span<TPixel> row = pixels.DangerousGetRowSpan(y);
if (typeof(TPixel) == typeof(Rgba32))
{
Span<ColorRgba32> rgba32Row = row.Cast<TPixel, ColorRgba32>();
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<T>();
#endif

TPixel pixel = new();
pixel.FromRgba32(Unsafe.As<ColorRgba32, Rgba32>(ref colors[y * header.Width + x]));
row[x] = pixel;
}
}
}
}
else
{
for (int y = 0; y < pixels.Height; y++)
{
Span<TPixel> 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;
}
}
}
}

/// <summary>
/// Shifts a number to the left, shifting the LSB in
/// </summary>
/// <param name="num">The number to shift</param>
/// <param name="amt">The amount of bits to shift</param>
/// <returns>The shifted number</returns>
[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<Rgba32>(options, stream, cancellationToken);
}

protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Span<byte> 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.");
}
15 changes: 15 additions & 0 deletions Refresh.GameServer/Importing/Gtf/GtfFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using SixLabors.ImageSharp.Formats;

namespace Refresh.GameServer.Importing.Gtf;

public class GtfFormat : IImageFormat<GtfMetadata>
{
public GtfMetadata CreateDefaultFormatMetadata() => new();

public string Name => "GTF";
public string DefaultMimeType => "";
public IEnumerable<string> MimeTypes => new string[] { };
public IEnumerable<string> FileExtensions => new[] { "GTF" };

public static IImageFormat Instance { get; } = new GtfFormat();
}
38 changes: 38 additions & 0 deletions Refresh.GameServer/Importing/Gtf/GtfHeader.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions Refresh.GameServer/Importing/Gtf/GtfMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Refresh.GameServer.Importing.Gtf;

public class GtfMetadata
{

}
77 changes: 77 additions & 0 deletions Refresh.GameServer/Importing/Gtf/GtfPixelFormat.cs
Original file line number Diff line number Diff line change
@@ -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)
};
}
}
8 changes: 8 additions & 0 deletions Refresh.GameServer/Importing/ImageImporter.Conversions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using ICSharpCode.SharpZipLib.Zip.Compression;
using Pfim;
using Refresh.GameServer.Importing.Gtf;
using SixLabors.ImageSharp.Formats;

namespace Refresh.GameServer.Importing;

Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion Refresh.GameServer/Importing/ImageImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void ImportFromDataStore(GameDatabaseContext context, IDataStore dataStor
List<GameAsset> 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));

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 8215e16

Please sign in to comment.