2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
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
This commit was merged in pull request #92.
This commit is contained in:
280
LightlessSync/Services/TextureCompression/TexFileHelper.cs
Normal file
280
LightlessSync/Services/TextureCompression/TexFileHelper.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user