All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 Changes: - Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more. - Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name. - Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much. - Chat has been added to the top menu, working in Zone or in Syncshells to be used there. - Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well. - Moved to the internal object table to have faster load times for users; people should load in faster - Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files - Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore. - Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all). - Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list. - Lightfinder plates have been moved away from using Nameplates, but will use an overlay. - Main UI has been changed a bit with a gradient, and on hover will glow up now. - Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items. - Reworked Settings UI to look more modern. - Performance should be better due to new systems that would dispose of the collections and better caching of items. Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: choco <choco@patat.nl> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Minmoose <KennethBohr@outlook.com> Reviewed-on: #92
281 lines
11 KiB
C#
281 lines
11 KiB
C#
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<byte> 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<TexFile.TexHeader>(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<byte>(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,
|
|
};
|
|
}
|