using System.Runtime.InteropServices; using Lumina.Data.Files; using OtterTex; namespace LightlessSync.Services.TextureCompression; // base taken from penumbra mostly internal static class TexFileHelper { private const int HeaderSize = 80; private const int MaxMipLevels = 13; public static ScratchImage Load(string path) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); return Load(stream); } public static ScratchImage Load(Stream stream) { using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true); var header = ReadHeader(reader); var meta = CreateMeta(header); meta.MipLevels = ComputeMipCount(stream.Length, header, meta); if (meta.MipLevels == 0) { throw new InvalidOperationException("TEX file does not contain a valid mip chain."); } var scratch = ScratchImage.Initialize(meta); ReadPixelData(reader, scratch); return scratch; } public static void Save(string path, ScratchImage image) { var header = BuildHeader(image); if (header.Format == TexFile.TextureFormat.Unknown) { throw new InvalidOperationException($"Unable to export TEX file with unsupported format {image.Meta.Format}."); } var mode = File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew; using var stream = new FileStream(path, mode, FileAccess.Write, FileShare.Read); using var writer = new BinaryWriter(stream); WriteHeader(writer, header); writer.Write(image.Pixels); GC.KeepAlive(image); } private static TexFile.TexHeader ReadHeader(BinaryReader reader) { Span buffer = stackalloc byte[HeaderSize]; var read = reader.Read(buffer); if (read != HeaderSize) { throw new EndOfStreamException($"Incomplete TEX header: expected {HeaderSize} bytes, read {read} bytes."); } return MemoryMarshal.Read(buffer); } private static TexMeta CreateMeta(in TexFile.TexHeader header) { var meta = new TexMeta { Width = header.Width, Height = header.Height, Depth = Math.Max(header.Depth, (ushort)1), ArraySize = 1, MipLevels = header.MipCount, Format = header.Format.ToDxgi(), Dimension = header.Type.ToDimension(), MiscFlags = header.Type.HasFlag(TexFile.Attribute.TextureTypeCube) ? D3DResourceMiscFlags.TextureCube : 0, MiscFlags2 = 0, }; if (meta.Format == DXGIFormat.Unknown) { throw new InvalidOperationException($"TEX format {header.Format} cannot be mapped to DXGI."); } if (meta.Dimension == TexDimension.Unknown) { throw new InvalidOperationException($"Unrecognised TEX dimension attribute {header.Type}."); } return meta; } private static unsafe int ComputeMipCount(long totalLength, in TexFile.TexHeader header, in TexMeta meta) { var width = Math.Max(meta.Width, 1); var height = Math.Max(meta.Height, 1); var minSide = meta.Format.IsCompressed() ? 4 : 1; var bitsPerPixel = meta.Format.BitsPerPixel(); var expectedOffset = HeaderSize; var remaining = totalLength - HeaderSize; for (var level = 0; level < MaxMipLevels; level++) { var declaredOffset = header.OffsetToSurface[level]; if (declaredOffset == 0) { return level; } if (declaredOffset != expectedOffset || remaining <= 0) { return level; } var mipSize = (int)((long)width * height * bitsPerPixel / 8); if (mipSize > remaining) { return level; } expectedOffset += mipSize; remaining -= mipSize; if (width <= minSide && height <= minSide) { return level + 1; } width = Math.Max(width / 2, minSide); height = Math.Max(height / 2, minSide); } return MaxMipLevels; } private static unsafe void ReadPixelData(BinaryReader reader, ScratchImage image) { fixed (byte* destination = image.Pixels) { var span = new Span(destination, image.Pixels.Length); var read = reader.Read(span); if (read < span.Length) { throw new InvalidDataException($"TEX pixel buffer is truncated (read {read} of {span.Length} bytes)."); } } } private static TexFile.TexHeader BuildHeader(ScratchImage image) { var meta = image.Meta; var header = new TexFile.TexHeader { Width = (ushort)meta.Width, Height = (ushort)meta.Height, Depth = (ushort)Math.Max(meta.Depth, 1), MipCount = (byte)Math.Min(meta.MipLevels, MaxMipLevels), Format = meta.Format.ToTex(), Type = meta.Dimension switch { _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, TexDimension.Tex1D => TexFile.Attribute.TextureType1D, TexDimension.Tex2D => TexFile.Attribute.TextureType2D, TexDimension.Tex3D => TexFile.Attribute.TextureType3D, _ => 0, }, }; PopulateOffsets(ref header, image); return header; } private static unsafe void PopulateOffsets(ref TexFile.TexHeader header, ScratchImage image) { var index = 0; fixed (byte* basePtr = image.Pixels) { foreach (var mip in image.Images) { if (index >= MaxMipLevels) { break; } var byteOffset = (byte*)mip.Pixels - basePtr; header.OffsetToSurface[index++] = HeaderSize + (uint)byteOffset; } } while (index < MaxMipLevels) { header.OffsetToSurface[index++] = 0; } header.LodOffset[0] = 0; header.LodOffset[1] = (byte)Math.Min(header.MipCount - 1, 1); header.LodOffset[2] = (byte)Math.Min(header.MipCount - 1, 2); } private static unsafe void WriteHeader(BinaryWriter writer, in TexFile.TexHeader header) { writer.Write((uint)header.Type); writer.Write((uint)header.Format); writer.Write(header.Width); writer.Write(header.Height); writer.Write(header.Depth); writer.Write((byte)(header.MipCount | (header.MipUnknownFlag ? 0x80 : 0))); writer.Write(header.ArraySize); writer.Write(header.LodOffset[0]); writer.Write(header.LodOffset[1]); writer.Write(header.LodOffset[2]); for (var i = 0; i < MaxMipLevels; i++) { writer.Write(header.OffsetToSurface[i]); } } private static TexDimension ToDimension(this TexFile.Attribute attribute) => (attribute & TexFile.Attribute.TextureTypeMask) switch { TexFile.Attribute.TextureType1D => TexDimension.Tex1D, TexFile.Attribute.TextureType2D => TexDimension.Tex2D, TexFile.Attribute.TextureType3D => TexDimension.Tex3D, _ => TexDimension.Unknown, }; private static DXGIFormat ToDxgi(this TexFile.TextureFormat format) => format switch { TexFile.TextureFormat.L8 => DXGIFormat.R8UNorm, TexFile.TextureFormat.A8 => DXGIFormat.A8UNorm, TexFile.TextureFormat.B4G4R4A4 => DXGIFormat.B4G4R4A4UNorm, TexFile.TextureFormat.B5G5R5A1 => DXGIFormat.B5G5R5A1UNorm, TexFile.TextureFormat.B8G8R8A8 => DXGIFormat.B8G8R8A8UNorm, TexFile.TextureFormat.B8G8R8X8 => DXGIFormat.B8G8R8X8UNorm, TexFile.TextureFormat.R32F => DXGIFormat.R32Float, TexFile.TextureFormat.R16G16F => DXGIFormat.R16G16Float, TexFile.TextureFormat.R32G32F => DXGIFormat.R32G32Float, TexFile.TextureFormat.R16G16B16A16F => DXGIFormat.R16G16B16A16Float, TexFile.TextureFormat.R32G32B32A32F => DXGIFormat.R32G32B32A32Float, TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, TexFile.TextureFormat.Shadow16 => DXGIFormat.R16Typeless, TexFile.TextureFormat.Shadow24 => DXGIFormat.R24G8Typeless, _ => DXGIFormat.Unknown, }; private static TexFile.TextureFormat ToTex(this DXGIFormat format) => format switch { DXGIFormat.R8UNorm => TexFile.TextureFormat.L8, DXGIFormat.A8UNorm => TexFile.TextureFormat.A8, DXGIFormat.B4G4R4A4UNorm => TexFile.TextureFormat.B4G4R4A4, DXGIFormat.B5G5R5A1UNorm => TexFile.TextureFormat.B5G5R5A1, DXGIFormat.B8G8R8A8UNorm => TexFile.TextureFormat.B8G8R8A8, DXGIFormat.B8G8R8X8UNorm => TexFile.TextureFormat.B8G8R8X8, DXGIFormat.R32Float => TexFile.TextureFormat.R32F, DXGIFormat.R16G16Float => TexFile.TextureFormat.R16G16F, DXGIFormat.R32G32Float => TexFile.TextureFormat.R32G32F, DXGIFormat.R16G16B16A16Float => TexFile.TextureFormat.R16G16B16A16F, DXGIFormat.R32G32B32A32Float => TexFile.TextureFormat.R32G32B32A32F, DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, DXGIFormat.R16Typeless => TexFile.TextureFormat.Shadow16, _ => TexFile.TextureFormat.Unknown, }; }