diff --git a/Penumbra/Import/Dds/DXT10Header.cs b/Penumbra/Import/Dds/DXT10Header.cs new file mode 100644 index 00000000..005cf24c --- /dev/null +++ b/Penumbra/Import/Dds/DXT10Header.cs @@ -0,0 +1,322 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +[StructLayout( LayoutKind.Sequential )] +public struct DXT10Header +{ + public DXGIFormat Format; + public D3DResourceDimension ResourceDimension; + public D3DResourceMiscFlags MiscFlags; + public uint ArraySize; + public D3DAlphaMode AlphaMode; + + public ParseType ToParseType() + { + return Format switch + { + DXGIFormat.BC1Typeless => ParseType.DXT1, + DXGIFormat.BC1UNorm => ParseType.DXT1, + DXGIFormat.BC1UNormSRGB => ParseType.DXT1, + + DXGIFormat.BC2Typeless => ParseType.DXT3, + DXGIFormat.BC2UNorm => ParseType.DXT3, + DXGIFormat.BC2UNormSRGB => ParseType.DXT3, + + DXGIFormat.BC3Typeless => ParseType.DXT5, + DXGIFormat.BC3UNorm => ParseType.DXT5, + DXGIFormat.BC3UNormSRGB => ParseType.DXT5, + + DXGIFormat.BC4Typeless => ParseType.BC4, + DXGIFormat.BC4SNorm => ParseType.BC4, + DXGIFormat.BC4UNorm => ParseType.BC4, + + DXGIFormat.BC5Typeless => ParseType.BC5, + DXGIFormat.BC5SNorm => ParseType.BC5, + DXGIFormat.BC5UNorm => ParseType.BC5, + + DXGIFormat.R8G8B8A8Typeless => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UNorm => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UNormSRGB => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UInt => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8SNorm => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8SInt => ParseType.R8G8B8A8, + + DXGIFormat.B8G8R8A8Typeless => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8A8UNorm => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8A8UNormSRGB => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8Typeless => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8UNorm => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8UNormSRGB => ParseType.B8G8R8A8, + + DXGIFormat.B4G4R4A4UNorm => ParseType.B4G4R4A4, + DXGIFormat.B5G5R5A1UNorm => ParseType.B5G5R5A1, + DXGIFormat.B5G6R5UNorm => ParseType.B5G6R5, + + DXGIFormat.BC6HSF16 => ParseType.Unsupported, + DXGIFormat.BC6HTypeless => ParseType.Unsupported, + DXGIFormat.BC6HUF16 => ParseType.Unsupported, + + DXGIFormat.BC7Typeless => ParseType.Unsupported, + DXGIFormat.BC7UNorm => ParseType.Unsupported, + DXGIFormat.BC7UNormSRGB => ParseType.Unsupported, + + DXGIFormat.Unknown => ParseType.Unsupported, + DXGIFormat.R32G32B32A32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32B32A32Float => ParseType.Unsupported, + DXGIFormat.R32G32B32A32UInt => ParseType.Unsupported, + DXGIFormat.R32G32B32A32SInt => ParseType.Unsupported, + DXGIFormat.R32G32B32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32B32Float => ParseType.Unsupported, + DXGIFormat.R32G32B32UInt => ParseType.Unsupported, + DXGIFormat.R32G32B32SInt => ParseType.Unsupported, + DXGIFormat.R16G16B16A16Typeless => ParseType.Unsupported, + DXGIFormat.R16G16B16A16Float => ParseType.Unsupported, + DXGIFormat.R16G16B16A16UNorm => ParseType.Unsupported, + DXGIFormat.R16G16B16A16UInt => ParseType.Unsupported, + DXGIFormat.R16G16B16A16SNorm => ParseType.Unsupported, + DXGIFormat.R16G16B16A16SInt => ParseType.Unsupported, + DXGIFormat.R32G32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32Float => ParseType.Unsupported, + DXGIFormat.R32G32UInt => ParseType.Unsupported, + DXGIFormat.R32G32SInt => ParseType.Unsupported, + DXGIFormat.R32G8X24Typeless => ParseType.Unsupported, + DXGIFormat.D32FloatS8X24UInt => ParseType.Unsupported, + DXGIFormat.R32FloatX8X24Typeless => ParseType.Unsupported, + DXGIFormat.X32TypelessG8X24UInt => ParseType.Unsupported, + DXGIFormat.R10G10B10A2Typeless => ParseType.Unsupported, + DXGIFormat.R10G10B10A2UNorm => ParseType.Unsupported, + DXGIFormat.R10G10B10A2UInt => ParseType.Unsupported, + DXGIFormat.R11G11B10Float => ParseType.Unsupported, + DXGIFormat.R16G16Typeless => ParseType.Unsupported, + DXGIFormat.R16G16Float => ParseType.Unsupported, + DXGIFormat.R16G16UNorm => ParseType.Unsupported, + DXGIFormat.R16G16UInt => ParseType.Unsupported, + DXGIFormat.R16G16SNorm => ParseType.Unsupported, + DXGIFormat.R16G16SInt => ParseType.Unsupported, + DXGIFormat.R32Typeless => ParseType.Unsupported, + DXGIFormat.D32Float => ParseType.Unsupported, + DXGIFormat.R32Float => ParseType.Unsupported, + DXGIFormat.R32UInt => ParseType.Unsupported, + DXGIFormat.R32SInt => ParseType.Unsupported, + DXGIFormat.R24G8Typeless => ParseType.Unsupported, + DXGIFormat.D24UNormS8UInt => ParseType.Unsupported, + DXGIFormat.R24UNormX8Typeless => ParseType.Unsupported, + DXGIFormat.X24TypelessG8UInt => ParseType.Unsupported, + DXGIFormat.R8G8Typeless => ParseType.Unsupported, + DXGIFormat.R8G8UNorm => ParseType.Unsupported, + DXGIFormat.R8G8UInt => ParseType.Unsupported, + DXGIFormat.R8G8SNorm => ParseType.Unsupported, + DXGIFormat.R8G8SInt => ParseType.Unsupported, + DXGIFormat.R16Typeless => ParseType.Unsupported, + DXGIFormat.R16Float => ParseType.Unsupported, + DXGIFormat.D16UNorm => ParseType.Unsupported, + DXGIFormat.R16UNorm => ParseType.Unsupported, + DXGIFormat.R16UInt => ParseType.Unsupported, + DXGIFormat.R16SNorm => ParseType.Unsupported, + DXGIFormat.R16SInt => ParseType.Unsupported, + DXGIFormat.R8Typeless => ParseType.Unsupported, + DXGIFormat.R8UNorm => ParseType.Unsupported, + DXGIFormat.R8UInt => ParseType.Unsupported, + DXGIFormat.R8SNorm => ParseType.Unsupported, + DXGIFormat.R8SInt => ParseType.Unsupported, + DXGIFormat.A8UNorm => ParseType.Unsupported, + DXGIFormat.R1UNorm => ParseType.Unsupported, + DXGIFormat.R9G9B9E5SharedEXP => ParseType.Unsupported, + DXGIFormat.R8G8B8G8UNorm => ParseType.Unsupported, + DXGIFormat.G8R8G8B8UNorm => ParseType.Unsupported, + DXGIFormat.R10G10B10XRBiasA2UNorm => ParseType.Unsupported, + DXGIFormat.AYUV => ParseType.Unsupported, + DXGIFormat.Y410 => ParseType.Unsupported, + DXGIFormat.Y416 => ParseType.Unsupported, + DXGIFormat.NV12 => ParseType.Unsupported, + DXGIFormat.P010 => ParseType.Unsupported, + DXGIFormat.P016 => ParseType.Unsupported, + DXGIFormat.F420Opaque => ParseType.Unsupported, + DXGIFormat.YUY2 => ParseType.Unsupported, + DXGIFormat.Y210 => ParseType.Unsupported, + DXGIFormat.Y216 => ParseType.Unsupported, + DXGIFormat.NV11 => ParseType.Unsupported, + DXGIFormat.AI44 => ParseType.Unsupported, + DXGIFormat.IA44 => ParseType.Unsupported, + DXGIFormat.P8 => ParseType.Unsupported, + DXGIFormat.A8P8 => ParseType.Unsupported, + DXGIFormat.P208 => ParseType.Unsupported, + DXGIFormat.V208 => ParseType.Unsupported, + DXGIFormat.V408 => ParseType.Unsupported, + DXGIFormat.SamplerFeedbackMinMipOpaque => ParseType.Unsupported, + DXGIFormat.SamplerFeedbackMipRegionUsedOpaque => ParseType.Unsupported, + DXGIFormat.ForceUInt => ParseType.Unsupported, + _ => ParseType.Unsupported, + }; + } + + public enum DXGIFormat : uint + { + Unknown = 0, + R32G32B32A32Typeless = 1, + R32G32B32A32Float = 2, + R32G32B32A32UInt = 3, + R32G32B32A32SInt = 4, + R32G32B32Typeless = 5, + R32G32B32Float = 6, + R32G32B32UInt = 7, + R32G32B32SInt = 8, + R16G16B16A16Typeless = 9, + R16G16B16A16Float = 10, + R16G16B16A16UNorm = 11, + R16G16B16A16UInt = 12, + R16G16B16A16SNorm = 13, + R16G16B16A16SInt = 14, + R32G32Typeless = 15, + R32G32Float = 16, + R32G32UInt = 17, + R32G32SInt = 18, + R32G8X24Typeless = 19, + D32FloatS8X24UInt = 20, + R32FloatX8X24Typeless = 21, + X32TypelessG8X24UInt = 22, + R10G10B10A2Typeless = 23, + R10G10B10A2UNorm = 24, + R10G10B10A2UInt = 25, + R11G11B10Float = 26, + R8G8B8A8Typeless = 27, + R8G8B8A8UNorm = 28, + R8G8B8A8UNormSRGB = 29, + R8G8B8A8UInt = 30, + R8G8B8A8SNorm = 31, + R8G8B8A8SInt = 32, + R16G16Typeless = 33, + R16G16Float = 34, + R16G16UNorm = 35, + R16G16UInt = 36, + R16G16SNorm = 37, + R16G16SInt = 38, + R32Typeless = 39, + D32Float = 40, + R32Float = 41, + R32UInt = 42, + R32SInt = 43, + R24G8Typeless = 44, + D24UNormS8UInt = 45, + R24UNormX8Typeless = 46, + X24TypelessG8UInt = 47, + R8G8Typeless = 48, + R8G8UNorm = 49, + R8G8UInt = 50, + R8G8SNorm = 51, + R8G8SInt = 52, + R16Typeless = 53, + R16Float = 54, + D16UNorm = 55, + R16UNorm = 56, + R16UInt = 57, + R16SNorm = 58, + R16SInt = 59, + R8Typeless = 60, + R8UNorm = 61, + R8UInt = 62, + R8SNorm = 63, + R8SInt = 64, + A8UNorm = 65, + R1UNorm = 66, + R9G9B9E5SharedEXP = 67, + R8G8B8G8UNorm = 68, + G8R8G8B8UNorm = 69, + BC1Typeless = 70, + BC1UNorm = 71, + BC1UNormSRGB = 72, + BC2Typeless = 73, + BC2UNorm = 74, + BC2UNormSRGB = 75, + BC3Typeless = 76, + BC3UNorm = 77, + BC3UNormSRGB = 78, + BC4Typeless = 79, + BC4UNorm = 80, + BC4SNorm = 81, + BC5Typeless = 82, + BC5UNorm = 83, + BC5SNorm = 84, + B5G6R5UNorm = 85, + B5G5R5A1UNorm = 86, + B8G8R8A8UNorm = 87, + B8G8R8X8UNorm = 88, + R10G10B10XRBiasA2UNorm = 89, + B8G8R8A8Typeless = 90, + B8G8R8A8UNormSRGB = 91, + B8G8R8X8Typeless = 92, + B8G8R8X8UNormSRGB = 93, + BC6HTypeless = 94, + BC6HUF16 = 95, + BC6HSF16 = 96, + BC7Typeless = 97, + BC7UNorm = 98, + BC7UNormSRGB = 99, + AYUV = 100, + Y410 = 101, + Y416 = 102, + NV12 = 103, + P010 = 104, + P016 = 105, + F420Opaque = 106, + YUY2 = 107, + Y210 = 108, + Y216 = 109, + NV11 = 110, + AI44 = 111, + IA44 = 112, + P8 = 113, + A8P8 = 114, + B4G4R4A4UNorm = 115, + P208 = 130, + V208 = 131, + V408 = 132, + SamplerFeedbackMinMipOpaque, + SamplerFeedbackMipRegionUsedOpaque, + ForceUInt = 0xffffffff, + } + + public enum D3DResourceDimension : int + { + Unknown = 0, + Buffer = 1, + Texture1D = 2, + Texture2D = 3, + Texture3D = 4, + } + + [Flags] + public enum D3DResourceMiscFlags : uint + { + GenerateMips = 0x000001, + Shared = 0x000002, + TextureCube = 0x000004, + DrawIndirectArgs = 0x000010, + BufferAllowRawViews = 0x000020, + BufferStructured = 0x000040, + ResourceClamp = 0x000080, + SharedKeyedMutex = 0x000100, + GDICompatible = 0x000200, + SharedNTHandle = 0x000800, + RestrictedContent = 0x001000, + RestrictSharedResource = 0x002000, + RestrictSharedResourceDriver = 0x004000, + Guarded = 0x008000, + TilePool = 0x020000, + Tiled = 0x040000, + HWProtected = 0x080000, + SharedDisplayable, + SharedExclusiveWriter, + }; + + public enum D3DAlphaMode : int + { + Unknown = 0, + Straight = 1, + Premultiplied = 2, + Opaque = 3, + Custom = 4, + }; +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs new file mode 100644 index 00000000..1b0f973a --- /dev/null +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Dalamud.Logging; +using Lumina.Data.Files; +using Lumina.Extensions; + +namespace Penumbra.Import.Dds; + +public class DdsFile +{ + public const int DdsIdentifier = 0x20534444; + + public readonly DdsHeader Header; + public readonly DXT10Header? DXT10Header; + private readonly byte[] _data; + + public ReadOnlySpan< byte > Data + => _data; + + public ReadOnlySpan< byte > MipMap( int level ) + { + var mipSize = ParseType switch + { + ParseType.Unsupported => 0, + ParseType.DXT1 => Header.Height * Header.Width / 2, + ParseType.BC4 => Header.Height * Header.Width / 2, + + ParseType.DXT3 => Header.Height * Header.Width, + ParseType.DXT5 => Header.Height * Header.Width, + ParseType.BC5 => Header.Height * Header.Width, + ParseType.Greyscale => Header.Height * Header.Width, + + ParseType.R4G4B4A4 => Header.Height * Header.Width * 2, + ParseType.B4G4R4A4 => Header.Height * Header.Width * 2, + ParseType.R5G5B5 => Header.Height * Header.Width * 2, + ParseType.B5G5R5 => Header.Height * Header.Width * 2, + ParseType.R5G6B5 => Header.Height * Header.Width * 2, + ParseType.B5G6R5 => Header.Height * Header.Width * 2, + ParseType.R5G5B5A1 => Header.Height * Header.Width * 2, + ParseType.B5G5R5A1 => Header.Height * Header.Width * 2, + + ParseType.R8G8B8 => Header.Height * Header.Width * 3, + ParseType.B8G8R8 => Header.Height * Header.Width * 3, + + ParseType.R8G8B8A8 => Header.Height * Header.Width * 4, + ParseType.B8G8R8A8 => Header.Height * Header.Width * 4, + _ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ), + }; + + if( Header.MipMapCount < level ) + { + throw new ArgumentOutOfRangeException( nameof( level ) ); + } + + var sum = 0; + for( var i = 0; i < level; ++i ) + { + sum += mipSize; + mipSize = Math.Max( 16, mipSize >> 2 ); + } + + + if( _data.Length < sum + mipSize ) + { + throw new Exception( "Not enough data to encode image." ); + } + + return _data.AsSpan( sum, mipSize ); + } + + private byte[]? _rgbaData; + public readonly ParseType ParseType; + + public ReadOnlySpan< byte > RgbaData + => _rgbaData ??= ParseToRgba(); + + private DdsFile( ParseType type, DdsHeader header, byte[] data, DXT10Header? dxt10Header = null ) + { + ParseType = type; + Header = header; + DXT10Header = dxt10Header; + _data = data; + } + + private byte[] ParseToRgba() + { + return ParseType switch + { + ParseType.Unsupported => Array.Empty< byte >(), + ParseType.DXT1 => ImageParsing.DecodeDxt1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.DXT3 => ImageParsing.DecodeDxt3( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.DXT5 => ImageParsing.DecodeDxt5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.BC4 => ImageParsing.DecodeBc4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.BC5 => ImageParsing.DecodeBc5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.Greyscale => ImageParsing.DecodeUncompressedGreyscale( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B4G4R4A4 => ImageParsing.DecodeUncompressedB4G4R4A4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G5B5 => ImageParsing.DecodeUncompressedR5G5B5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G5R5 => ImageParsing.DecodeUncompressedB5G5R5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G6B5 => ImageParsing.DecodeUncompressedR5G6B5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G6R5 => ImageParsing.DecodeUncompressedB5G6R5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G5R5A1 => ImageParsing.DecodeUncompressedB5G5R5A1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R8G8B8 => ImageParsing.DecodeUncompressedR8G8B8( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B8G8R8 => ImageParsing.DecodeUncompressedB8G8R8( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R8G8B8A8 => _data.Length == Header.Width * Header.Height * 4 ? _data : _data[ ..( Header.Width * Header.Height * 4 ) ], + ParseType.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( MipMap( 0 ), Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file ) + { + file = null; + try + { + using var br = new BinaryReader( data ); + if( br.ReadUInt32() != DdsIdentifier ) + { + return false; + } + + var header = br.ReadStructure< DdsHeader >(); + var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; + var type = header.PixelFormat.ToParseType( dxt10 ); + + file = new DdsFile( type, header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), dxt10 ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load DDS file:\n{e}" ); + return false; + } + } + + public bool ConvertToTex( out byte[] texBytes ) + { + using var mem = new MemoryStream( _data.Length * 2 ); + using( var bw = new BinaryWriter( mem ) ) + { + var (format, mipLength) = WriteTexHeader( bw ); + var bytes = format == TexFile.TextureFormat.R8G8B8X8 ? RgbaData : _data; + + if( bytes.Length < mipLength ) + { + throw new Exception( "Broken file. Not enough data." ); + } + + bw.Write( _data.AsSpan( 0, mipLength ) ); + } + + texBytes = mem.ToArray(); + return true; + } + + private (TexFile.TextureFormat, int) WriteTexHeader( BinaryWriter bw ) + { + var (format, mipLength) = ConvertFormat( ParseType, Header.Height, Header.Width ); + if( mipLength == 0 ) + { + throw new Exception( "Invalid format to convert to tex." ); + } + + var mipCount = Header.MipMapCount; + if( format == TexFile.TextureFormat.R8G8B8X8 && ParseType != ParseType.R8G8B8A8 ) + { + mipCount = 1; + } + + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )format ); + bw.Write( ( ushort )Header.Width ); + bw.Write( ( ushort )Header.Height ); + bw.Write( ( ushort )Header.Depth ); + bw.Write( ( ushort )mipCount ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + + var offset = 80; + var mipLengthSum = 0; + for( var i = 0; i < mipCount; ++i ) + { + bw.Write( offset ); + offset += mipLength; + mipLengthSum += mipLength; + mipLength = Math.Max( 16, mipLength >> 2 ); + } + + for( var i = mipCount; i < 13; ++i ) + { + bw.Write( 0 ); + } + + return ( format, mipLengthSum ); + } + + public static (TexFile.TextureFormat, int) ConvertFormat( ParseType type, int height, int width ) + { + return type switch + { + ParseType.Unsupported => ( TexFile.TextureFormat.Unknown, 0 ), + ParseType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ), + ParseType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 2 ), + ParseType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width * 2 ), + ParseType.BC4 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.BC5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.Greyscale => ( TexFile.TextureFormat.A8, height * width ), + ParseType.R4G4B4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), + ParseType.B4G4R4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), + ParseType.R5G5B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B5G5R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R5G6B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B5G6R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R5G5B5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), + ParseType.B5G5R5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), + ParseType.R8G8B8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B8G8R8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R8G8B8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B8G8R8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; + } +} + +public class TmpTexFile +{ + public TexFile.TexHeader Header; + public byte[] RgbaData; + + public void Load( BinaryReader br ) + { + Header = br.ReadStructure< TexFile.TexHeader >(); + var data = br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ); + RgbaData = Header.Format switch + { + TexFile.TextureFormat.L8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT1 => ImageParsing.DecodeDxt1( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT3 => ImageParsing.DecodeDxt3( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT5 => ImageParsing.DecodeDxt5( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8R8G8B8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R8G8B8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/DdsHeader.cs b/Penumbra/Import/Dds/DdsHeader.cs new file mode 100644 index 00000000..30f976d4 --- /dev/null +++ b/Penumbra/Import/Dds/DdsHeader.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +[StructLayout( LayoutKind.Sequential )] +public struct DdsHeader +{ + public const int Size = 124; + public const uint MagicNumber = 'D' | ( 'D' << 8 ) | ( 'S' << 16 ) | ( ' ' << 24 ); + + private int _size; + public DdsFlags Flags; + public int Height; + public int Width; + public int PitchOrLinearSize; + public int Depth; + public int MipMapCount; + public int Reserved1; + public int Reserved2; + public int Reserved3; + public int Reserved4; + public int Reserved5; + public int Reserved6; + public int Reserved7; + public int Reserved8; + public int Reserved9; + public int ReservedA; + public int ReservedB; + public PixelFormat PixelFormat; + public DdsCaps1 Caps1; + public DdsCaps2 Caps2; + public uint Caps3; + public uint Caps4; + public int ReservedC; + + [Flags] + public enum DdsFlags : uint + { + Caps = 0x00000001, + Height = 0x00000002, + Width = 0x00000004, + Pitch = 0x00000008, + PixelFormat = 0x00001000, + MipMapCount = 0x00020000, + LinearSize = 0x00080000, + Depth = 0x00800000, + + Required = Caps | Height | Width | PixelFormat, + } + + [Flags] + public enum DdsCaps1 : uint + { + Complex = 0x08, + MipMap = 0x400000, + Texture = 0x1000, + } + + [Flags] + public enum DdsCaps2 : uint + { + CubeMap = 0x200, + CubeMapPositiveEX = 0x400, + CubeMapNegativeEX = 0x800, + CubeMapPositiveEY = 0x1000, + CubeMapNegativeEY = 0x2000, + CubeMapPositiveEZ = 0x4000, + CubeMapNegativeEZ = 0x8000, + Volume = 0x200000, + } + + public void Write( BinaryWriter bw ) + { + bw.Write( MagicNumber ); + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( Height ); + bw.Write( Width ); + bw.Write( PitchOrLinearSize ); + bw.Write( Depth ); + bw.Write( MipMapCount ); + bw.Write( Reserved1 ); + bw.Write( Reserved2 ); + bw.Write( Reserved3 ); + bw.Write( Reserved4 ); + bw.Write( Reserved5 ); + bw.Write( Reserved6 ); + bw.Write( Reserved7 ); + bw.Write( Reserved8 ); + bw.Write( Reserved9 ); + bw.Write( ReservedA ); + bw.Write( ReservedB ); + PixelFormat.Write( bw ); + bw.Write( ( uint )Caps1 ); + bw.Write( ( uint )Caps2 ); + bw.Write( Caps3 ); + bw.Write( Caps4 ); + bw.Write( ReservedC ); + } + + public void Write( byte[] bytes, int offset ) + { + using var m = new MemoryStream( bytes, offset, bytes.Length - offset ); + using var bw = new BinaryWriter( m ); + Write( bw ); + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/ImageParsing.cs b/Penumbra/Import/Dds/ImageParsing.cs new file mode 100644 index 00000000..c3bdd482 --- /dev/null +++ b/Penumbra/Import/Dds/ImageParsing.cs @@ -0,0 +1,580 @@ +using System; +using System.Runtime.CompilerServices; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Dds; + +public static partial class ImageParsing +{ + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static Rgba32 Get565Color( ushort c ) + { + var ret = new Rgba32 + { + R = ( byte )( c & 0x1F ), + G = ( byte )( ( c >> 5 ) & 0x3F ), + B = ( byte )( c >> 11 ), + A = 0xFF, + }; + + ret.R = ( byte )( ( ret.R << 3 ) | ( ret.R >> 2 ) ); + ret.G = ( byte )( ( ret.G << 2 ) | ( ret.G >> 3 ) ); + ret.B = ( byte )( ( ret.B << 3 ) | ( ret.B >> 2 ) ); + + return ret; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static (Rgba32, Rgba32) GetDxt1CombinedColors( bool c1Larger, Rgba32 c1, Rgba32 c2 ) + { + if( c1Larger ) + { + static byte C( byte a1, byte a2 ) + => ( byte )( ( 2 * a1 + a2 ) / 3 ); + + return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ), + new Rgba32( C( c2.R, c1.R ), C( c2.G, c1.G ), C( c2.B, c1.B ) ) ); + } + else + { + static byte C( byte a1, byte a2 ) + => ( byte )( ( a1 + a2 ) / 2 ); + + return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ), + new Rgba32( 0, 0, 0, 0 ) ); + } + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color, byte alpha ) + { + *ptr++ = color.R; + *ptr++ = color.G; + *ptr++ = color.B; + *ptr++ = alpha; + return ptr; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color ) + => CopyBytes( ptr, color, color.A ); + + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytesAlphaDown( byte* ptr, Rgba32 color, byte alpha ) + => CopyBytes( ptr, color, ( byte )( ( alpha & 0x0F ) | ( alpha << 4 ) ) ); + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytesAlphaUp( byte* ptr, Rgba32 color, byte alpha ) + => CopyBytes( ptr, color, ( byte )( ( alpha & 0xF0 ) | ( alpha >> 4 ) ) ); + + private static void Verify( ReadOnlySpan< byte > data, int height, int width, int blockSize, int bytes ) + { + if( data.Length % bytes != 0 ) + { + throw new ArgumentException( $"Length {data.Length} not a multiple of {bytes} bytes.", nameof( data ) ); + } + + if( height * width > data.Length * blockSize * blockSize / bytes ) + { + throw new ArgumentException( $"Not enough data encoded in {data.Length} to fill image of dimensions {height} * {width}.", + nameof( data ) ); + } + + if( height % blockSize != 0 ) + { + throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) ); + } + + if( width % blockSize != 0 ) + { + throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) ); + } + } + + private static unsafe byte* GetDxt1Colors( byte* ptr, Span< Rgba32 > colors ) + { + var c1 = ( ushort )( *ptr | ( ptr[ 1 ] << 8 ) ); + var c2 = ( ushort )( ptr[ 2 ] | ( ptr[ 3 ] << 8 ) ); + colors[ 0 ] = Get565Color( c1 ); + colors[ 1 ] = Get565Color( c2 ); + ( colors[ 2 ], colors[ 3 ] ) = GetDxt1CombinedColors( c1 > c2, colors[ 0 ], colors[ 1 ] ); + return ptr + 4; + } + + public static unsafe byte[] DecodeDxt1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 8 ); + + var ret = new byte[data.Length * 8]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + + fixed( byte* r = ret, d = data ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = GetDxt1Colors( inputPtr, colors ); + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytes( outputPtr2, colors[ colorMask & 0b11 ] ); + outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ] ); + outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ] ); + CopyBytes( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ] ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeDxt3( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + + fixed( byte* r = ret, d = data ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + var alphaPtr = inputPtr; + inputPtr = GetDxt1Colors( inputPtr + 8, colors ); + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr ); + outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr ); + CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ ); + } + } + } + } + + return ret; + } + + private static unsafe byte* Dxt5AlphaTable( byte* ptr, Span< byte > alphaValues ) + { + var alphaLookup = stackalloc byte[8]; + alphaLookup[ 0 ] = *ptr++; + alphaLookup[ 1 ] = *ptr++; + if( alphaLookup[ 0 ] > alphaLookup[ 1 ] ) + { + alphaLookup[ 2 ] = ( byte )( ( 6 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 3 ] = ( byte )( ( 5 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 4 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 5 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 4 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 6 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 5 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 7 ] = ( byte )( ( alphaLookup[ 0 ] + 6 * alphaLookup[ 1 ] ) / 7 ); + } + else + { + alphaLookup[ 2 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 3 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 4 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 5 ] = ( byte )( ( alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 6 ] = byte.MinValue; + alphaLookup[ 7 ] = byte.MaxValue; + } + + var alphaLong = ( ulong )*ptr++; + alphaLong |= ( ulong )*ptr++ << 8; + alphaLong |= ( ulong )*ptr++ << 16; + alphaLong |= ( ulong )*ptr++ << 24; + alphaLong |= ( ulong )*ptr++ << 32; + alphaLong |= ( ulong )*ptr++ << 40; + + for( var i = 0; i < 16; ++i ) + { + alphaValues[ i ] = alphaLookup[ ( alphaLong >> ( i * 3 ) ) & 0x07 ]; + } + + return ptr; + } + + public static unsafe byte[] DecodeDxt5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + Span< byte > alphaValues = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = alphaValues ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, alphaValues ); + inputPtr = GetDxt1Colors( inputPtr, colors ); + var alphaPtr = a; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr++ ); + CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeBc4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 8 ); + var ret = new byte[data.Length * 8]; + Span< byte > channelValues = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = channelValues ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, channelValues ); + var channelPtr = a; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeBc5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< byte > channel1 = stackalloc byte[16]; + Span< byte > channel2 = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = channel1, b = channel2 ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, channel1 ); + inputPtr = Dxt5AlphaTable( inputPtr, channel2 ); + var channel1Ptr = a; + var channel2Ptr = b; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedGreyscale( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 1 ); + var ret = new byte[data.Length * 4]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + *ptr++ = *input; + *ptr++ = *input; + *ptr++ = *input++; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR4G4B4A4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var input = ( ushort* )d; + foreach( var b in data ) + { + *ptr++ = ( byte )( ( b << 4 ) | ( b & 0x0F ) ); + *ptr++ = ( byte )( ( b >> 4 ) | ( b & 0xF0 ) ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB4G4R4A4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( ( b >> 8 ) & 0x0F ) | ( ( b >> 4 ) & 0xF0 ) ); + *ptr++ = ( byte )( ( b & 0xF0 ) | ( ( b >> 4 ) & 0x0F ) ); + + *ptr++ = ( byte )( ( b & 0x0F ) | ( b << 4 ) ); + *ptr++ = ( byte )( ( ( b >> 8 ) & 0xF0 ) | ( b >> 12 ) ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G5B5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G5R5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G6B5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x07E0; + *ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) ); + tmp = b & 0xF800; + *ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G6R5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0xF800; + *ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) ); + tmp = b & 0x07E0; + *ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G5B5A1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + *ptr++ = 0xFF; + *ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G5R5A1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR8G8B8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 3 ); + var ret = new byte[data.Length * 4 / 3]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + *ptr++ = *input++; + *ptr++ = *input++; + *ptr++ = *input++; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB8G8R8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 3 ); + var ret = new byte[data.Length * 4 / 3]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + var b = *input++; + var g = *input++; + *ptr++ = *input++; + *ptr++ = g; + *ptr++ = b; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static byte[] DecodeUncompressedR8G8B8A8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 4 ); + var ret = new byte[data.Length]; + data.CopyTo( ret ); + return ret; + } + + public static unsafe byte[] DecodeUncompressedB8G8R8A8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 4 ); + var ret = new byte[data.Length]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + var b = *input++; + var g = *input++; + *ptr++ = *input++; + *ptr++ = g; + *ptr++ = b; + *ptr++ = *input++; + } + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/PixelFormat.cs b/Penumbra/Import/Dds/PixelFormat.cs new file mode 100644 index 00000000..6c99860e --- /dev/null +++ b/Penumbra/Import/Dds/PixelFormat.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +public enum ParseType +{ + Unsupported, + DXT1, + DXT3, + DXT5, + BC4, + BC5, + + Greyscale, + R4G4B4A4, + B4G4R4A4, + R5G5B5, + B5G5R5, + R5G6B5, + B5G6R5, + R5G5B5A1, + B5G5R5A1, + R8G8B8, + B8G8R8, + R8G8B8A8, + B8G8R8A8, +} + +[StructLayout( LayoutKind.Sequential )] +public struct PixelFormat +{ + public int Size; + public FormatFlags Flags; + public FourCCType FourCC; + public int RgbBitCount; + public uint RBitMask; + public uint GBitMask; + public uint BBitMask; + public uint ABitMask; + + + [Flags] + public enum FormatFlags : uint + { + AlphaPixels = 0x000001, + Alpha = 0x000002, + FourCC = 0x000004, + RGB = 0x000040, + YUV = 0x000200, + Luminance = 0x020000, + } + + public enum FourCCType : uint + { + NoCompression = 0, + DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ), + DXT2 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '2' << 24 ), + DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ), + DXT4 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '4' << 24 ), + DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ), + DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ), + ATI1 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '1' << 24 ), + BC4U = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( 'U' << 24 ), + BC45 = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( '5' << 24 ), + ATI2 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '2' << 24 ), + BC5U = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( 'U' << 24 ), + BC55 = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( '5' << 24 ), + } + + + + public void Write( BinaryWriter bw ) + { + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( ( uint )FourCC ); + bw.Write( RgbBitCount ); + bw.Write( RBitMask ); + bw.Write( GBitMask ); + bw.Write( BBitMask ); + bw.Write( ABitMask ); + } + + public ParseType ToParseType( DXT10Header? dxt10 ) + { + return FourCC switch + { + FourCCType.NoCompression => HandleUncompressed(), + FourCCType.DXT1 => ParseType.DXT1, + FourCCType.DXT2 => ParseType.Unsupported, + FourCCType.DXT3 => ParseType.DXT3, + FourCCType.DXT4 => ParseType.Unsupported, + FourCCType.DXT5 => ParseType.DXT5, + FourCCType.DX10 => dxt10?.ToParseType() ?? ParseType.Unsupported, + FourCCType.ATI1 => ParseType.BC4, + FourCCType.BC4U => ParseType.BC4, + FourCCType.BC45 => ParseType.BC4, + FourCCType.ATI2 => ParseType.BC5, + FourCCType.BC5U => ParseType.BC5, + FourCCType.BC55 => ParseType.BC5, + _ => ParseType.Unsupported, + }; + } + + private ParseType HandleUncompressed() + { + switch( RgbBitCount ) + { + case 8: return ParseType.Greyscale; + case 16: + if( ABitMask == 0xF000 ) + { + return RBitMask > GBitMask ? ParseType.B4G4R4A4 : ParseType.R4G4B4A4; + } + + if( Flags.HasFlag( FormatFlags.AlphaPixels ) ) + { + return RBitMask > GBitMask ? ParseType.B5G5R5A1 : ParseType.R5G5B5A1; + } + + if( GBitMask == 0x07E0 ) + { + return RBitMask > GBitMask ? ParseType.B5G6R5 : ParseType.R5G6B5; + } + + return RBitMask > GBitMask ? ParseType.B5G5R5 : ParseType.R5G5B5; + case 24: return RBitMask > GBitMask ? ParseType.B8G8R8 : ParseType.R8G8B8; + case 32: return RBitMask > GBitMask ? ParseType.B8G8R8A8 : ParseType.R8G8B8A8; + default: return ParseType.Unsupported; + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs new file mode 100644 index 00000000..7a28ee7a --- /dev/null +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using Lumina.Data.Files; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Dds; + +public class TextureImporter +{ + private static void WriteHeader( byte[] target, int width, int height ) + { + using var mem = new MemoryStream( target ); + using var bw = new BinaryWriter( mem ); + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); + bw.Write( ( ushort )width ); + bw.Write( ( ushort )height ); + bw.Write( ( ushort )1 ); + bw.Write( ( ushort )1 ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + bw.Write( 80 ); + for( var i = 1; i < 13; ++i ) + { + bw.Write( 0 ); + } + } + + public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData ) + { + var header = new DdsHeader() + { + Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap, + Depth = 1, + Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount, + Height = height, + Width = width, + PixelFormat = new PixelFormat() + { + Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB, + FourCC = 0, + BBitMask = 0x000000FF, + GBitMask = 0x0000FF00, + RBitMask = 0x00FF0000, + ABitMask = 0xFF000000, + Size = 32, + RgbBitCount = 32, + }, + }; + ddsData = new byte[4 + DdsHeader.Size + rgba.Length]; + header.Write( ddsData, 0 ); + rgba.CopyTo( ddsData, DdsHeader.Size + 4 ); + for( var i = 0; i < rgba.Length; i += 4 ) + { + ( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] ) + = ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] ); + } + + return true; + } + + public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) + { + texData = Array.Empty< byte >(); + if( rgba.Length != width * height * 4 ) + { + return false; + } + + texData = new byte[80 + width * height * 4]; + WriteHeader( texData, width, height ); + // RGBA to BGRA. + for( var i = 0; i < rgba.Length; i += 4 ) + { + texData[ 80 + i + 0 ] = rgba[ i + 2 ]; + texData[ 80 + i + 1 ] = rgba[ i + 1 ]; + texData[ 80 + i + 2 ] = rgba[ i + 0 ]; + texData[ 80 + i + 3 ] = rgba[ i + 3 ]; + } + + return true; + } + + public static bool PngToTex( string inputFile, out byte[] texData ) + { + using var file = File.OpenRead( inputFile ); + var image = Image.Load< Bgra32 >( file ); + + var buffer = new byte[80 + image.Height * image.Width * 4]; + WriteHeader( buffer, image.Width, image.Height ); + + var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); + image.CopyPixelDataTo( span ); + + texData = buffer; + return true; + } + + public void Import( string inputFile ) + { } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs deleted file mode 100644 index 99708dc8..00000000 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ /dev/null @@ -1,553 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Runtime.InteropServices; -using Dalamud.Logging; -using Lumina.Data.Files; -using Lumina.Extensions; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Penumbra.Import.Textures; - -[StructLayout( LayoutKind.Sequential )] -public struct PixelFormat -{ - [Flags] - public enum FormatFlags : uint - { - AlphaPixels = 0x000001, - Alpha = 0x000002, - FourCC = 0x000004, - RGB = 0x000040, - YUV = 0x000200, - Luminance = 0x020000, - } - - public enum FourCCType : uint - { - DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ), - DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ), - DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ), - DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ), - } - - public int Size; - public FormatFlags Flags; - public FourCCType FourCC; - public int RgbBitCount; - public uint RBitMask; - public uint GBitMask; - public uint BBitMask; - public uint ABitMask; - - public void Write( BinaryWriter bw ) - { - bw.Write( Size ); - bw.Write( ( uint )Flags ); - bw.Write( ( uint )FourCC ); - bw.Write( RgbBitCount ); - bw.Write( RBitMask ); - bw.Write( GBitMask ); - bw.Write( BBitMask ); - bw.Write( ABitMask ); - } -} - -[StructLayout( LayoutKind.Sequential )] -public struct DdsHeader -{ - [Flags] - public enum DdsFlags : uint - { - Caps = 0x00000001, - Height = 0x00000002, - Width = 0x00000004, - Pitch = 0x00000008, - PixelFormat = 0x00001000, - MipMapCount = 0x00020000, - LinearSize = 0x00080000, - Depth = 0x00800000, - - Required = Caps | Height | Width | PixelFormat, - } - - [Flags] - public enum DdsCaps1 : uint - { - Complex = 0x08, - MipMap = 0x400000, - Texture = 0x1000, - } - - [Flags] - public enum DdsCaps2 : uint - { - CubeMap = 0x200, - CubeMapPositiveEX = 0x400, - CubeMapNegativeEX = 0x800, - CubeMapPositiveEY = 0x1000, - CubeMapNegativeEY = 0x2000, - CubeMapPositiveEZ = 0x4000, - CubeMapNegativeEZ = 0x8000, - Volume = 0x200000, - } - - public const int Size = 124; - private int _size; - public DdsFlags Flags; - public int Height; - public int Width; - public int PitchOrLinearSize; - public int Depth; - public int MipMapCount; - public int Reserved1; - public int Reserved2; - public int Reserved3; - public int Reserved4; - public int Reserved5; - public int Reserved6; - public int Reserved7; - public int Reserved8; - public int Reserved9; - public int ReservedA; - public int ReservedB; - public PixelFormat PixelFormat; - public DdsCaps1 Caps1; - public DdsCaps2 Caps2; - public uint Caps3; - public uint Caps4; - public int ReservedC; - - public void Write( BinaryWriter bw ) - { - bw.Write( ( byte )'D' ); - bw.Write( ( byte )'D' ); - bw.Write( ( byte )'S' ); - bw.Write( ( byte )' ' ); - bw.Write( Size ); - bw.Write( ( uint )Flags ); - bw.Write( Height ); - bw.Write( Width ); - bw.Write( PitchOrLinearSize ); - bw.Write( Depth ); - bw.Write( MipMapCount ); - bw.Write( Reserved1 ); - bw.Write( Reserved2 ); - bw.Write( Reserved3 ); - bw.Write( Reserved4 ); - bw.Write( Reserved5 ); - bw.Write( Reserved6 ); - bw.Write( Reserved7 ); - bw.Write( Reserved8 ); - bw.Write( Reserved9 ); - bw.Write( ReservedA ); - bw.Write( ReservedB ); - PixelFormat.Write( bw ); - bw.Write( ( uint )Caps1 ); - bw.Write( ( uint )Caps2 ); - bw.Write( Caps3 ); - bw.Write( Caps4 ); - bw.Write( ReservedC ); - } - - public void Write( byte[] bytes, int offset ) - { - using var m = new MemoryStream( bytes, offset, bytes.Length - offset ); - using var bw = new BinaryWriter( m ); - Write( bw ); - } -} - -[StructLayout( LayoutKind.Sequential )] -public struct DXT10Header -{ - public enum DXGIFormat : uint - { - Unknown = 0, - R32G32B32A32Typeless = 1, - R32G32B32A32Float = 2, - R32G32B32A32UInt = 3, - R32G32B32A32SInt = 4, - R32G32B32Typeless = 5, - R32G32B32Float = 6, - R32G32B32UInt = 7, - R32G32B32SInt = 8, - R16G16B16A16Typeless = 9, - R16G16B16A16Float = 10, - R16G16B16A16UNorm = 11, - R16G16B16A16UInt = 12, - R16G16B16A16SNorm = 13, - R16G16B16A16SInt = 14, - R32G32Typeless = 15, - R32G32Float = 16, - R32G32UInt = 17, - R32G32SInt = 18, - R32G8X24Typeless = 19, - D32FloatS8X24UInt = 20, - R32FloatX8X24Typeless = 21, - X32TypelessG8X24UInt = 22, - R10G10B10A2Typeless = 23, - R10G10B10A2UNorm = 24, - R10G10B10A2UInt = 25, - R11G11B10Float = 26, - R8G8B8A8Typeless = 27, - R8G8B8A8UNorm = 28, - R8G8B8A8UNormSRGB = 29, - R8G8B8A8UInt = 30, - R8G8B8A8SNorm = 31, - R8G8B8A8SInt = 32, - R16G16Typeless = 33, - R16G16Float = 34, - R16G16UNorm = 35, - R16G16UInt = 36, - R16G16SNorm = 37, - R16G16SInt = 38, - R32Typeless = 39, - D32Float = 40, - R32Float = 41, - R32UInt = 42, - R32SInt = 43, - R24G8Typeless = 44, - D24UNormS8UInt = 45, - R24UNormX8Typeless = 46, - X24TypelessG8UInt = 47, - R8G8Typeless = 48, - R8G8UNorm = 49, - R8G8UInt = 50, - R8G8SNorm = 51, - R8G8SInt = 52, - R16Typeless = 53, - R16Float = 54, - D16UNorm = 55, - R16UNorm = 56, - R16UInt = 57, - R16SNorm = 58, - R16SInt = 59, - R8Typeless = 60, - R8UNorm = 61, - R8UInt = 62, - R8SNorm = 63, - R8SInt = 64, - A8UNorm = 65, - R1UNorm = 66, - R9G9B9E5SharedEXP = 67, - R8G8B8G8UNorm = 68, - G8R8G8B8UNorm = 69, - BC1Typeless = 70, - BC1UNorm = 71, - BC1UNormSRGB = 72, - BC2Typeless = 73, - BC2UNorm = 74, - BC2UNormSRGB = 75, - BC3Typeless = 76, - BC3UNorm = 77, - BC3UNormSRGB = 78, - BC4Typeless = 79, - BC4UNorm = 80, - BC4SNorm = 81, - BC5Typeless = 82, - BC5UNorm = 83, - BC5SNorm = 84, - B5G6R5UNorm = 85, - B5G5R5A1UNorm = 86, - B8G8R8A8UNorm = 87, - B8G8R8X8UNorm = 88, - R10G10B10XRBiasA2UNorm = 89, - B8G8R8A8Typeless = 90, - B8G8R8A8UNormSRGB = 91, - B8G8R8X8Typeless = 92, - B8G8R8X8UNormSRGB = 93, - BC6HTypeless = 94, - BC6HUF16 = 95, - BC6HSF16 = 96, - BC7Typeless = 97, - BC7UNorm = 98, - BC7UNormSRGB = 99, - AYUV = 100, - Y410 = 101, - Y416 = 102, - NV12 = 103, - P010 = 104, - P016 = 105, - F420Opaque = 106, - YUY2 = 107, - Y210 = 108, - Y216 = 109, - NV11 = 110, - AI44 = 111, - IA44 = 112, - P8 = 113, - A8P8 = 114, - B4G4R4A4UNorm = 115, - P208 = 130, - V208 = 131, - V408 = 132, - SamplerFeedbackMinMipOpaque, - SamplerFeedbackMipRegionUsedOpaque, - ForceUInt = 0xffffffff, - } - - public enum D3DResourceDimension : int - { - Unknown = 0, - Buffer = 1, - Texture1D = 2, - Texture2D = 3, - Texture3D = 4, - } - - [Flags] - public enum D3DResourceMiscFlags : uint - { - GenerateMips = 0x000001, - Shared = 0x000002, - TextureCube = 0x000004, - DrawIndirectArgs = 0x000010, - BufferAllowRawViews = 0x000020, - BufferStructured = 0x000040, - ResourceClamp = 0x000080, - SharedKeyedMutex = 0x000100, - GDICompatible = 0x000200, - SharedNTHandle = 0x000800, - RestrictedContent = 0x001000, - RestrictSharedResource = 0x002000, - RestrictSharedResourceDriver = 0x004000, - Guarded = 0x008000, - TilePool = 0x020000, - Tiled = 0x040000, - HWProtected = 0x080000, - SharedDisplayable, - SharedExclusiveWriter, - }; - - public enum D3DAlphaMode : int - { - Unknown = 0, - Straight = 1, - Premultiplied = 2, - Opaque = 3, - Custom = 4, - }; - - public DXGIFormat Format; - public D3DResourceDimension ResourceDimension; - public D3DResourceMiscFlags MiscFlags; - public uint ArraySize; - public D3DAlphaMode AlphaMode; -} - -public class DdsFile -{ - public const int DdsIdentifier = 0x20534444; - - public DdsHeader Header; - public DXT10Header? DXT10Header; - public byte[] MainSurfaceData; - public byte[] RemainingSurfaceData; - - private DdsFile( DdsHeader header, byte[] mainSurfaceData, byte[] remainingSurfaceData, DXT10Header? dXT10Header = null ) - { - Header = header; - DXT10Header = dXT10Header; - MainSurfaceData = mainSurfaceData; - RemainingSurfaceData = remainingSurfaceData; - } - - public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file ) - { - file = null; - try - { - using var br = new BinaryReader( data ); - if( br.ReadUInt32() != DdsIdentifier ) - { - return false; - } - - var header = br.ReadStructure< DdsHeader >(); - var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; - - file = new DdsFile( header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), Array.Empty< byte >(), - dxt10 ); - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load DDS file:\n{e}" ); - return false; - } - } - - public bool ConvertToTex( out byte[] texBytes ) - { - using var mem = new MemoryStream( MainSurfaceData.Length * 2 ); - using( var bw = new BinaryWriter( mem ) ) - { - var format = WriteTexHeader( bw ); - bw.Write( ConvertBytes( MainSurfaceData, format ) ); - } - - texBytes = mem.ToArray(); - return true; - } - - private TexFile.TextureFormat WriteTexHeader( BinaryWriter bw ) - { - var (format, mipLength) = ConvertFormat( Header.PixelFormat, Header.Height, Header.Width, DXT10Header ); - - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )format ); - bw.Write( ( ushort )Header.Width ); - bw.Write( ( ushort )Header.Height ); - bw.Write( ( ushort )Header.Depth ); - bw.Write( ( ushort )Header.MipMapCount ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - - var offset = 80; - for( var i = 0; i < Header.MipMapCount; ++i ) - { - bw.Write( offset ); - offset += mipLength; - mipLength = Math.Max( 16, mipLength >> 2 ); - } - - for( var i = Header.MipMapCount; i < 13; ++i ) - { - bw.Write( 0 ); - } - - return format; - } - - private static byte[] ConvertBytes( byte[] ddsData, TexFile.TextureFormat format ) - { - return format switch - { - _ => ddsData, - }; - } - - private static (TexFile.TextureFormat, int) ConvertFormat( PixelFormat format, int height, int width, DXT10Header? dxt10 ) - => format.FourCC switch - { - PixelFormat.FourCCType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ), - PixelFormat.FourCCType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 4 ), - PixelFormat.FourCCType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width ), - PixelFormat.FourCCType.DX10 => dxt10!.Value.Format switch - { - Textures.DXT10Header.DXGIFormat.A8UNorm => ( TexFile.TextureFormat.A8, height * width ), - Textures.DXT10Header.DXGIFormat.R8G8B8A8UInt => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R8G8UNorm => ( TexFile.TextureFormat.L8, height * width ), - Textures.DXT10Header.DXGIFormat.B8G8R8X8UNorm => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.B4G4R4A4UNorm => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.B5G5R5A1UNorm => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.R32Float => ( TexFile.TextureFormat.R32F, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R32G32B32A32Float => ( TexFile.TextureFormat.R32G32B32A32F, height * width * 16 ), - Textures.DXT10Header.DXGIFormat.R16G16Float => ( TexFile.TextureFormat.R16G16F, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R16G16B16A16Float => ( TexFile.TextureFormat.R16G16B16A16F, height * width * 8 ), - Textures.DXT10Header.DXGIFormat.D16UNorm => ( TexFile.TextureFormat.D16, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.D24UNormS8UInt => ( TexFile.TextureFormat.D24S8, height * width * 4 ), - _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - }, - _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - }; -} - -public class TextureImporter -{ - private static void WriteHeader( byte[] target, int width, int height ) - { - using var mem = new MemoryStream( target ); - using var bw = new BinaryWriter( mem ); - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); - bw.Write( ( ushort )width ); - bw.Write( ( ushort )height ); - bw.Write( ( ushort )1 ); - bw.Write( ( ushort )1 ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - bw.Write( 80 ); - for( var i = 1; i < 13; ++i ) - { - bw.Write( 0 ); - } - } - - public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData ) - { - var header = new DdsHeader() - { - Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap, - Depth = 1, - Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount, - Height = height, - Width = width, - PixelFormat = new PixelFormat() - { - Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB, - FourCC = 0, - BBitMask = 0x000000FF, - GBitMask = 0x0000FF00, - RBitMask = 0x00FF0000, - ABitMask = 0xFF000000, - Size = 32, - RgbBitCount = 32, - }, - }; - ddsData = new byte[DdsHeader.Size + rgba.Length]; - header.Write( ddsData, 0 ); - rgba.CopyTo( ddsData, DdsHeader.Size ); - for( var i = 0; i < rgba.Length; i += 4 ) - { - ( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] ) - = ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] ); - } - - return true; - } - - public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) - { - texData = Array.Empty< byte >(); - if( rgba.Length != width * height * 4 ) - { - return false; - } - - texData = new byte[80 + width * height * 4]; - WriteHeader( texData, width, height ); - // RGBA to BGRA. - for( var i = 0; i < rgba.Length; i += 4 ) - { - texData[ 80 + i + 0 ] = rgba[ i + 2 ]; - texData[ 80 + i + 1 ] = rgba[ i + 1 ]; - texData[ 80 + i + 2 ] = rgba[ i + 0 ]; - texData[ 80 + i + 3 ] = rgba[ i + 3 ]; - } - - return true; - } - - public static bool PngToTex( string inputFile, out byte[] texData ) - { - using var file = File.OpenRead( inputFile ); - var image = Image.Load< Bgra32 >( file ); - - var buffer = new byte[80 + image.Height * image.Width * 4]; - WriteHeader( buffer, image.Width, image.Height ); - - var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); - image.CopyPixelDataTo( span ); - - texData = buffer; - return true; - } - - public void Import( string inputFile ) - { } -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 9a5c8f16..6f0ec77f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; @@ -13,7 +14,7 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; -using Penumbra.Import.Textures; +using Penumbra.Import.Dds; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; @@ -35,6 +36,10 @@ public partial class ModEditWindow private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; + private bool _invertLeft = false; + private bool _invertRight = false; + private int _offsetX = 0; + private int _offsetY = 0; private readonly FileDialogManager _dialogManager = new(); @@ -147,19 +152,7 @@ public partial class ModEditWindow return ( null, 0, 0 ); } - f.ConvertToTex( out var bytes ); - TexFile tex = new(); - tex.GetType().GetProperty( "Data", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, bytes ); - tex.GetType().GetProperty( "FileStream", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, new MemoryStream( tex.Data ) ); - tex.GetType().GetProperty( "Reader", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, new BinaryReader( tex.FileStream ) ); - tex.LoadFile(); - return ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); + return ( f.RgbaData.ToArray(), f.Header.Width, f.Header.Height ); } catch( Exception e ) { @@ -172,10 +165,26 @@ public partial class ModEditWindow { try { + if( fromDisk ) + { + var tmp = new TmpTexFile(); + using var stream = File.OpenRead( path ); + using var br = new BinaryReader( stream ); + tmp.Load(br); + return (tmp.RgbaData, tmp.Header.Width, tmp.Header.Height); + } + + var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path ); - return tex == null - ? ( null, 0, 0 ) - : ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); + if( tex == null ) + { + return ( null, 0, 0 ); + } + + var rgba = tex.Header.Format == TexFile.TextureFormat.A8R8G8B8 + ? ImageParsing.DecodeUncompressedR8G8B8A8( tex.ImageData, tex.Header.Height, tex.Header.Width ) + : tex.GetRgbaImageData(); + return ( rgba, tex.Header.Width, tex.Header.Height ); } catch( Exception e ) { @@ -250,7 +259,7 @@ public partial class ModEditWindow UpdateCenter(); } - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform ) + private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) { if( bytes == null ) { @@ -259,6 +268,11 @@ public partial class ModEditWindow var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); var transformed = Vector4.Transform( rgba.ToVector4(), transform ); + if( invert ) + { + transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); + } + transformed.X = Math.Clamp( transformed.X, 0, 1 ); transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); @@ -266,11 +280,32 @@ public partial class ModEditWindow return transformed; } + private Vector4 DataLeft( int offset ) + => CappedVector( _imageLeft, offset, _multiplierLeft, _invertLeft ); + + private Vector4 DataRight( int x, int y ) + { + if( _imageRight == null ) + { + return Vector4.Zero; + } + + x -= _offsetX; + y -= _offsetY; + if( x < 0 || x >= _wrapRight!.Width || y < 0 || y >= _wrapRight!.Height ) + { + return Vector4.Zero; + } + + var offset = ( y * _wrapRight!.Width + x ) * 4; + return CappedVector( _imageRight, offset, _multiplierRight, _invertRight ); + } + private void AddPixels( int width, int x, int y ) { - var offset = ( y * width + x ) * 4; - var left = CappedVector( _imageLeft, offset, _multiplierLeft ); - var right = CappedVector( _imageRight, offset, _multiplierRight ); + var offset = ( width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); var alpha = right.W + left.W * ( 1 - right.W ); if( alpha == 0 ) { @@ -287,14 +322,14 @@ public partial class ModEditWindow private void UpdateCenter() { - if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity ) + if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity && !_invertLeft ) { _imageCenter = _imageLeft; _wrapCenter = _wrapLeft; return; } - if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity ) + if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity && !_invertRight ) { _imageCenter = _imageRight; _wrapCenter = _wrapRight; @@ -308,22 +343,19 @@ public partial class ModEditWindow if( _imageLeft != null || _imageRight != null ) { - var (width, height) = _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); - if( _imageRight == null || _wrapRight!.Width == width && _wrapRight!.Height == height ) + var (totalWidth, totalHeight) = + _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); + _imageCenter = new byte[4 * totalWidth * totalHeight]; + + Parallel.For( 0, totalHeight - 1, ( y, _ ) => { - _imageCenter = new byte[4 * width * height]; - - for( var y = 0; y < height; ++y ) + for( var x = 0; x < totalWidth; ++x ) { - for( var x = 0; x < width; ++x ) - { - AddPixels( width, x, y ); - } + AddPixels( totalWidth, x, y ); } - - _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, width, height, 4 ); - return; - } + } ); + _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, totalWidth, totalHeight, 4 ); + return; } _imageCenter = null; @@ -334,6 +366,7 @@ public partial class ModEditWindow { if( wrap != null ) { + ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" ); size = size with { Y = wrap.Height * size.X / wrap.Width }; ImGui.Image( wrap.ImGuiHandle, size ); } @@ -397,7 +430,7 @@ public partial class ModEditWindow { _dialogManager.Draw(); - using var tab = ImRaii.TabItem( "Texture Import/Export" ); + using var tab = ImRaii.TabItem( "Texture Import/Export (WIP)" ); if( !tab ) { return; @@ -412,7 +445,7 @@ public partial class ModEditWindow PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 ); ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) ) + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) ) { UpdateCenter(); } @@ -465,7 +498,7 @@ public partial class ModEditWindow PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 ); ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) ) + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) || ImGui.Checkbox( "Invert##Right", ref _invertRight ) ) { UpdateCenter(); }