Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once).

This commit is contained in:
Ottermandias 2025-06-08 11:28:12 +02:00
parent 4c0e6d2a67
commit a16fd85a7e
5 changed files with 117 additions and 1 deletions

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Services;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip;
@ -146,6 +147,9 @@ public partial class TexToolsImporter
case ".mtrl":
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
case ".tex":
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
break;
default:
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
break;

View file

@ -259,6 +259,7 @@ public partial class TexToolsImporter
{
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
_ => data.Data,
};

View file

@ -61,6 +61,75 @@ public static class TexFileParser
return 13;
}
public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize)
{
var width = (uint)header.Width;
var height = (uint)header.Height;
var format = header.Format.ToDXGI();
var bits = format.BitsPerPixel();
var totalSize = 80u;
size -= totalSize;
var minSize = format.IsCompressed() ? 4u : 1u;
for (var i = 0; i < 13; ++i)
{
var requiredSize = (uint)((long)width * height * bits / 8);
if (requiredSize > size)
{
newSize = totalSize;
if (header.MipCount != i)
{
Penumbra.Log.Debug(
$"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed.");
FixLodOffsets(ref header, i);
}
return;
}
if (header.OffsetToSurface[i] != totalSize)
{
Penumbra.Log.Debug(
$"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed.");
header.OffsetToSurface[i] = totalSize;
}
if (width == minSize && height == minSize)
{
newSize = totalSize;
if (header.MipCount != i)
{
Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints.");
FixLodOffsets(ref header, i);
}
return;
}
totalSize += requiredSize;
size -= requiredSize;
width = Math.Max(width / 2, minSize);
height = Math.Max(height / 2, minSize);
}
newSize = totalSize;
if (header.MipCount != 13)
{
Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed.");
FixLodOffsets(ref header, 13);
}
void FixLodOffsets(ref TexFile.TexHeader header, int index)
{
header.MipCount = index;
if (header.LodOffset[2] >= header.MipCount)
header.LodOffset[2] = (byte)(header.MipCount - 1);
if (header.LodOffset[1] >= header.MipCount)
header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1);
for (++index; index < 13; ++index)
header.OffsetToSurface[index] = 0;
}
}
private static unsafe void CopyData(ScratchImage image, BinaryReader r)
{
fixed (byte* ptr = image.Pixels)

View file

@ -70,7 +70,6 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
_import = null;
}
public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod)
{
if (!_modsToAdd.TryDequeue(out var directory))

View file

@ -1,6 +1,10 @@
using Dalamud.Interface.ImGuiNotification;
using Lumina.Data.Files;
using OtterGui.Classes;
using OtterGui.Services;
using Lumina.Extensions;
using Penumbra.GameData.Files.Utility;
using Penumbra.Import.Textures;
using SharpCompress.Common;
using SharpCompress.Readers;
using MdlFile = Penumbra.GameData.Files.MdlFile;
@ -296,6 +300,26 @@ public class MigrationManager(Configuration config) : IService
}
}
public void FixMipMaps(IReader reader, string directory, ExtractionOptions options)
{
var path = Path.Combine(directory, reader.Entry.Key!);
using var s = new MemoryStream();
using var e = reader.OpenEntryStream();
e.CopyTo(s);
var length = s.Position;
s.Seek(0, SeekOrigin.Begin);
var br = new BinaryReader(s, Encoding.UTF8, true);
var header = br.ReadStructure<TexFile.TexHeader>();
br.Dispose();
TexFileParser.FixMipOffsets(length, ref header, out var actualSize);
s.Seek(0, SeekOrigin.Begin);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
using var f = File.Open(path, FileMode.Create, FileAccess.Write);
f.Write(header);
f.Write(s.GetBuffer().AsSpan(80, (int)actualSize - 80));
}
/// <summary> Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. </summary>
public byte[] MigrateTtmpModel(string path, byte[] data)
{
@ -348,6 +372,25 @@ public class MigrationManager(Configuration config) : IService
}
}
public byte[] FixTtmpMipMaps(string path, byte[] data)
{
using var m = new MemoryStream(data);
var br = new BinaryReader(m, Encoding.UTF8, true);
var header = br.ReadStructure<TexFile.TexHeader>();
br.Dispose();
TexFileParser.FixMipOffsets(data.Length, ref header, out var actualSize);
if (actualSize == data.Length)
return data;
var ret = new byte[actualSize];
using var m2 = new MemoryStream(ret);
using var bw = new BinaryWriter(m2);
bw.Write(header);
bw.Write(data.AsSpan(80, (int)actualSize - 80));
return ret;
}
private static bool MigrateModel(string path, MdlFile mdl, bool createBackup)
{