Skip to content

Commit

Permalink
Merge pull request #44 from Soreepeong/feature/texture-planes
Browse files Browse the repository at this point in the history
Add support for multiple mipmaps/depths for TexFile
  • Loading branch information
NotAdam authored Jun 23, 2022
2 parents 5c459c6 + 4f0969b commit a30c1f6
Show file tree
Hide file tree
Showing 13 changed files with 882 additions and 133 deletions.
264 changes: 131 additions & 133 deletions src/Lumina/Data/Files/TexFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Lumina.Data.Attributes;
using Lumina.Data.Parsing.Tex;
using Lumina.Data.Parsing.Tex.Buffers;
using Lumina.Extensions;

// ReSharper disable InconsistentNaming

namespace Lumina.Data.Files
{
[FileExtension( ".tex" )]
public class TexFile : FileResource
{
[Flags]
public enum Attribute : uint
{
DiscardPerFrame = 0x1,
Expand All @@ -37,9 +39,16 @@ public enum Attribute : uint
TextureNoSwizzle = 0x80000000,
}

/// <summary>
/// Texture formats. Channel ordering in name follows the enumeration in DXGI_FORMAT.
///
/// Excerpt from: https://docs.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format
/// > Most formats have byte-aligned components, and the components are in C-array order (the least address comes first).
/// > For those formats that don't have power-of-2-aligned components, the first named component is in the least-significant bits.
/// </summary>
[Flags]
public enum TextureFormat
{
Unknown = 0x0,
TypeShift = 0xC,
TypeMask = 0xF000,
ComponentShift = 0x8,
Expand All @@ -51,32 +60,58 @@ public enum TextureFormat
TypeInteger = 0x1,
TypeFloat = 0x2,
TypeDxt = 0x3,
TypeBc123 = 0x3,
TypeDepthStencil = 0x4,
TypeSpecial = 0x5,
TypeBc57 = 0x6,

Unknown = 0x0,

// Integer types
L8 = 0x1130,
A8 = 0x1131,
B4G4R4A4 = 0x1440,
B5G5R5A1 = 0x1441,
B8G8R8A8 = 0x1450,
B8G8R8X8 = 0x1451,

[Obsolete("Use B4G4R4A4 instead.")]
R4G4B4A4 = 0x1440,
[Obsolete("Use B5G5R5A1 instead.")]
R5G5B5A1 = 0x1441,
[Obsolete("Use B8G8R8A8 instead.")]
A8R8G8B8 = 0x1450,

// todo:
[Obsolete("Use B8G8R8X8 instead.")]
R8G8B8X8 = 0x1451,
[Obsolete("Not supported by Windows DirectX 11 version of the game, nor have any mention of the value, as of 6.15.")]
A8R8G8B82 = 0x1452,
R4G4B4A4 = 0x1440,
R5G5B5A1 = 0x1441,
L8 = 0x1130,

// todo:
A8 = 0x1131,

// todo:

// Floating point types
R32F = 0x2150,
R32G32B32A32F = 0x2470,
R16G16F = 0x2250,
R32G32F = 0x2260,
R16G16B16A16F = 0x2460,
R32G32B32A32F = 0x2470,

// Block compression types (DX9 names)
DXT1 = 0x3420,
DXT3 = 0x3430,
DXT5 = 0x3431,
ATI2 = 0x6230,

// Block compression types (DX11 names)
BC1 = 0x3420,
BC2 = 0x3430,
BC3 = 0x3431,
BC5 = 0x6230,
BC7 = 0x6432,

// Depth stencil types
// Does not exist in ffxiv_dx11.exe: RGBA8 0x4401
D16 = 0x4140,
D24S8 = 0x4250,

//todo: RGBA8 0x4401
// Special types
Null = 0x5100,
Shadow16 = 0x5140,
Shadow24 = 0x5150,
Expand All @@ -95,146 +130,109 @@ public unsafe struct TexHeader
public fixed uint OffsetToSurface[13];
};

public TexHeader Header;

public int HeaderLength => Unsafe.SizeOf< TexHeader >();

/// <summary>
/// The converted A8R8G8B8 image, in bytes.
/// Specify preprocessing texture data for consumption in DXGI.
/// </summary>
public byte[] ImageData { get; private set; }

public override void LoadFile()
{
Reader.BaseStream.Position = 0;
Header = Reader.ReadStructure< TexHeader >();

// todo: this isn't correct and reads out the whole data portion as 1 image instead of accounting for lod levels
// probably a breaking change to fix this
ImageData = Convert( DataSpan.Slice( HeaderLength ), Header.Width, Header.Height );
}

// converts various formats to A8R8G8B8
private byte[] Convert( Span< byte > src, int width, int height )
{
byte[] dst = new byte[width * height * 4];

switch( Header.Format )
{
case TextureFormat.DXT1:
ProcessDxt1( src, dst, width, height );
break;
case TextureFormat.DXT3:
ProcessDxt3( src, dst, width, height );
break;
case TextureFormat.DXT5:
ProcessDxt5( src, dst, width, height );
break;
case TextureFormat.R16G16B16A16F:
ProcessA16R16G16B16_Float( src, dst, width, height );
break;
case TextureFormat.R5G5B5A1:
ProcessA1R5G5B5( src, dst, width, height );
break;
case TextureFormat.R4G4B4A4:
ProcessA4R4G4B4( src, dst, width, height );
break;
case TextureFormat.L8:
ProcessR3G3B2( src, dst, width, height );
break;
case TextureFormat.A8R8G8B8:
ProcessA8R8G8B8( src, dst, width, height );
break;
default:
throw new NotImplementedException( $"TextureFormat {Header.Format.ToString()} is not supported for image conversion." );
}

return dst;
}

// #region shamelessly copied from coinach
// might be slowed down by src copying when calling squish
private static void ProcessA16R16G16B16_Float( Span< byte > src, byte[] dst, int width, int height )
public enum DxgiFormatConversion
{
// Clipping can, and will occur since values go outside 0..1
for( var i = 0; i < width * height; ++i )
{
var srcOff = i * 4 * 2;
var dstOff = i * 4;

for( var j = 0; j < 4; ++j )
dst[ dstOff + j ] = (byte)( src.Unpack( srcOff + j * 2 ) * byte.MaxValue );
}
/// <summary>
/// No conversion is required.
/// </summary>
NoConversion,

/// <summary>
/// Conversion from L8 (8bpp) to B8G8R8A8 (32bpp) is required.
/// Each byte indicates color value for each RGB channel. Alpha channel is fixed to 255.
/// </summary>
FromL8ToB8G8R8A8,

/// <summary>
/// Conversion from B4G4R4A4 (16bpp) to B8G8R8A8 (32bpp) is required.
/// </summary>
FromB4G4R4A4ToB8G8R8A8,

/// <summary>
/// Conversion from B5G5R5A1 (16bpp) to B8G8R8A8 (32bpp) is required.
/// </summary>
FromB5G5R5A1ToB8G8R8A8,
}

private static void ProcessA1R5G5B5( Span< byte > src, byte[] dst, int width, int height )
{
for( var i = 0; ( i + 2 ) <= 2 * width * height; i += 2 )
{
var v = BitConverter.ToUInt16( src.Slice( i, sizeof( UInt16 ) ).ToArray(), 0 );
private byte[]? _imageDataDefault;

var a = (uint)( v & 0x8000 );
var r = (uint)( v & 0x7C00 );
var g = (uint)( v & 0x03E0 );
var b = (uint)( v & 0x001F );
public TexHeader Header;

var rgb = ( ( r << 9 ) | ( g << 6 ) | ( b << 3 ) );
var argbValue = ( a * 0x1FE00 | rgb | ( ( rgb >> 5 ) & 0x070707 ) );
public int HeaderLength => Unsafe.SizeOf< TexHeader >();

for( var j = 0; j < 4; ++j )
dst[ i * 2 + j ] = (byte)( argbValue >> ( 8 * j ) );
}
}
/// <summary>
/// Parsed texture buffer, in original texture format.
/// </summary>
public TextureBuffer TextureBuffer;

private static void ProcessA4R4G4B4( Span< byte > src, byte[] dst, int width, int height )
/// <summary>
/// The converted A8R8G8B8 image, taking the first Z/face/mipmap.
/// </summary>
public byte[] ImageData
{
for( var i = 0; ( i + 2 ) <= 2 * width * height; i += 2 )
get
{
var v = BitConverter.ToUInt16( src.Slice( i, sizeof( UInt16 ) ).ToArray(), 0 );

for( var j = 0; j < 4; ++j )
dst[ i * 2 + j ] = (byte)( ( ( v >> ( 4 * j ) ) & 0x0F ) << 4 );
_imageDataDefault ??= TextureBuffer.Filter( mip: 0, z: 0, format: TextureFormat.B8G8R8A8 ).RawData;
return _imageDataDefault;
}
}

private static void ProcessA8R8G8B8( Span< byte > src, byte[] dst, int width, int height )
{
// Some transparent images have larger dst lengths than their src.
var length = Math.Min( src.Length, dst.Length );
src.Slice( 0, length ).CopyTo( dst.AsSpan() );
}

private static void ProcessDxt1( Span< byte > src, byte[] dst, int width, int height )
public override void LoadFile()
{
var dec = Squish.DecompressImage( src.ToArray(), width, height, SquishOptions.DXT1 );
Array.Copy( dec, dst, dst.Length );
}
Reader.BaseStream.Position = 0;
Header = Reader.ReadStructure< TexHeader >();

private static void ProcessDxt3( Span< byte > src, byte[] dst, int width, int height )
{
var dec = Squish.DecompressImage( src.ToArray(), width, height, SquishOptions.DXT3 );
Array.Copy( dec, dst, dst.Length );
}
if( ( Header.Type & Attribute.TextureTypeCube ) != 0 && Header.Depth != 1 )
throw new NotSupportedException( "Cube map texture with depth value above 1 is currently not supported." );

private static void ProcessDxt5( Span< byte > src, byte[] dst, int width, int height )
{
var dec = Squish.DecompressImage( src.ToArray(), width, height, SquishOptions.DXT5 );
Array.Copy( dec, dst, dst.Length );
TextureBuffer = TextureBuffer.FromStream( Header, Reader );
}

private static void ProcessR3G3B2( Span< byte > src, byte[] dst, int width, int height )
/// <summary>
/// Get DXGI_FORMAT and required preprocessing from TextureFormat.
/// </summary>
/// <param name="format">.tex texture format value.</param>
/// <param name="useGameCompatible">Whether to emulate the game on preprocessing texture data.</param>
/// <remarks>
/// Values are taken from v6.15 ffxiv_dx11.exe+0x321f80.
/// </remarks>
public static Tuple< int, DxgiFormatConversion > GetDxgiFormatFromTextureFormat( TextureFormat format, bool useGameCompatible = true )
{
for( var i = 0; i < width * height; ++i )
return format switch
{
var r = (uint)( src[ i ] & 0xE0 );
var g = (uint)( src[ i ] & 0x1C );
var b = (uint)( src[ i ] & 0x03 );

dst[ i * 4 + 0 ] = (byte)( b | ( b << 2 ) | ( b << 4 ) | ( b << 6 ) );
dst[ i * 4 + 1 ] = (byte)( g | ( g << 3 ) | ( g << 6 ) );
dst[ i * 4 + 2 ] = (byte)( r | ( r << 3 ) | ( r << 6 ) );
dst[ i * 4 + 3 ] = 0xFF;
}
TextureFormat.Unknown => Tuple.Create( 0x00, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_UNKNOWN
TextureFormat.Null => Tuple.Create( 0x00, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_UNKNOWN
TextureFormat.R32G32B32A32F => Tuple.Create( 0x02, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R32G32B32A32_FLOAT
TextureFormat.R16G16B16A16F => Tuple.Create( 0x0a, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R16G16B16A16_FLOAT
TextureFormat.R32G32F => Tuple.Create( 0x10, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R32G32_FLOAT
TextureFormat.R16G16F => Tuple.Create( 0x22, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R16G16_FLOAT
TextureFormat.R32F => Tuple.Create( 0x29, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R32_FLOAT
TextureFormat.D24S8 => Tuple.Create( 0x2c, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R24G8_TYPELESS
TextureFormat.Shadow24 => Tuple.Create( 0x2c, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R24G8_TYPELESS
TextureFormat.D16 => Tuple.Create( 0x35, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R16_TYPELESS
TextureFormat.Shadow16 => Tuple.Create( 0x35, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_R16_TYPELESS
TextureFormat.A8 => Tuple.Create( 0x41, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_A8_UNORM
TextureFormat.BC1 => Tuple.Create( 0x47, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_BC1_UNORM
TextureFormat.BC2 => Tuple.Create( 0x4a, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_BC2_UNORM
TextureFormat.BC3 => Tuple.Create( 0x4d, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_BC3_UNORM
TextureFormat.BC5 => Tuple.Create( 0x53, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_BC5_UNORM
TextureFormat.L8 => Tuple.Create( 0x57, DxgiFormatConversion.FromL8ToB8G8R8A8 ), // each pixel is RGBA(x, x, x, 255)
TextureFormat.B4G4R4A4 => useGameCompatible
? Tuple.Create( 0x57, DxgiFormatConversion.FromB4G4R4A4ToB8G8R8A8 ) // DXGI_FORMAT_B8G8R8A8_UNORM
: Tuple.Create( 0x73, DxgiFormatConversion.NoConversion ) // DXGI_FORMAT_B4G4R4A4_UNORM
, // DXGI_FORMAT_B4G4R4A4_UNORM(0x73): unsupported in dx10, dx10.1, dx11, and dx11.1 (before windows8)
TextureFormat.B5G5R5A1 =>useGameCompatible
? Tuple.Create( 0x57, DxgiFormatConversion.FromB5G5R5A1ToB8G8R8A8 ) // DXGI_FORMAT_B8G8R8A8_UNORM
: Tuple.Create( 0x56, DxgiFormatConversion.NoConversion ) // DXGI_FORMAT_B5G5R5A1_UNORM
, // DXGI_FORMAT_B5G5R5A1_UNORM(0x56): unsupported in dx10, dx10.1, dx11, and dx11.1 (before windows8)
TextureFormat.B8G8R8A8 => Tuple.Create( 0x57, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_B8G8R8A8_UNORM
TextureFormat.B8G8R8X8 => Tuple.Create( 0x58, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_B8G8R8X8_UNORM
TextureFormat.BC7 => Tuple.Create( 0x62, DxgiFormatConversion.NoConversion ), // DXGI_FORMAT_BC7_UNORM
_ => throw new NotSupportedException($"TextureFormat {(int)format:X04} is not supported."),
};
}
}
}
35 changes: 35 additions & 0 deletions src/Lumina/Data/Parsing/Tex/Buffers/A8TextureBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;

namespace Lumina.Data.Parsing.Tex.Buffers
{
/// <summary>
/// Represent a face in .tex file, in A8 texture format.
/// </summary>
public class A8TextureBuffer : TextureBuffer
{
/// <inheritdoc />
public A8TextureBuffer( bool isDepthConstant, int width, int height, int depth, int[] mipmapAllocations, byte[] buffer )
: base( isDepthConstant, width, height, depth, mipmapAllocations, buffer )
{
}

/// <inheritdoc />
public override int NumBytesOfMipmapPerPlane( int mipmapIndex ) => NumPixelsOfMipmapPerPlane( mipmapIndex );

/// <inheritdoc />
protected override unsafe void ConvertToB8G8R8A8( byte[] buffer, int destOffset, int sourceOffset, int width, int height, int depth )
{
fixed( byte* dstb = buffer, srcb = RawData )
{
var src = new Span< byte >( srcb + sourceOffset, width * height * depth );
var dst = new Span< uint >( dstb + destOffset, width * height * depth );
for( var i = 0; i < dst.Length; i++ )
dst[ i ] = 0x1000000U * src[ i ];
}
}

/// <inheritdoc />
protected override TextureBuffer CreateNew( bool isDepthConstant, int width, int height, int depth, int[] mipmapAllocations, byte[] buffer )
=> new A8TextureBuffer( isDepthConstant, width, height, depth, mipmapAllocations, buffer );
}
}
Loading

0 comments on commit a30c1f6

Please sign in to comment.