diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0cb854f3..8a27f02b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData; @@ -23,17 +24,17 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal) + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal); } - private ResourceNode? CreateNodeFromTex(nint sourceAddress, ByteString gamePath, bool @internal, bool dx11) + private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) { if (dx11) { @@ -59,13 +60,19 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); } - private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) - => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); + private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal) + { + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + } + + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, bool withName) { var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; @@ -79,13 +86,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, sourceAddress, gamePaths[0], fullPath, @internal); + return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal); + return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { @@ -95,12 +103,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (!Utf8GamePath.FromString(path, out var gamePath)) return null; - return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false); + return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false); } public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { - var node = CreateNodeFromResourceHandle(ResourceType.Imc, (nint) imc, imc, true, false); + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); if (node == null) return null; @@ -113,8 +121,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return node; } - public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + => CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) { @@ -145,6 +153,38 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { + static ushort GetTextureIndex(ushort texFlags) + { + if ((texFlags & 0x001F) != 0x001F) + return (ushort)(texFlags & 0x001F); + else if ((texFlags & 0x03E0) != 0x03E0) + return (ushort)((texFlags >> 5) & 0x001F); + else + return (ushort)((texFlags >> 10) & 0x001F); + } + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) + { + var textures = mtrl->Textures; + for (var i = 0; i < mtrl->TextureCount; ++i) + { + if (textures[i].ResourceHandle == handle) + return textures[i].Id; + } + + return null; + } + static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) + { + var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers; + for (var i = 0; i < shpk->SamplerCount; ++i) + { + if (samplers[i].Id == id) + return samplers[i].Crc; + } + + return null; + } + if (mtrl == null) return null; @@ -153,23 +193,36 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (node == null) return null; - var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; - - var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); + var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; + var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { - var texNode = CreateNodeFromTex(nint.Zero, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); if (texNode == null) continue; if (WithNames) { - var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; + string? name = null; + if (shpk != null) + { + var index = GetTextureIndex(resource->TexSpace[i].Flags); + uint? samplerId; + if (index != 0x001F) + samplerId = mtrl->Textures[index].Id; + else + samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); + if (samplerId.HasValue) + { + var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); + if (samplerCrc.HasValue) + name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + } + } node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else @@ -181,6 +234,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return node; } + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonResourceHandle == null) + return null; + + return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + } + private FullPath FilterFullPath(FullPath fullPath) { if (!fullPath.IsRooted) @@ -294,4 +355,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return name; } + + static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + { + if (handle == null) + return 0; + + return ResourceHandle.GetLength(handle); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dc0c5fcb..bceda36c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -9,37 +9,43 @@ public class ResourceNode { public readonly string? Name; public readonly ResourceType Type; - public readonly nint SourceAddress; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; public readonly Utf8GamePath GamePath; public readonly Utf8GamePath[] PossibleGamePaths; public readonly FullPath FullPath; + public readonly ulong Length; public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = gamePath; + Name = name; + Type = type; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; + GamePath = gamePath; PossibleGamePaths = new[] { gamePath, }; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, - bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + ulong length, bool @internal) { Name = name; Type = type; - SourceAddress = sourceAddress; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; PossibleGamePaths = possibleGamePaths; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } @@ -48,10 +54,12 @@ public class ResourceNode { Name = name; Type = originalResourceNode.Type; - SourceAddress = originalResourceNode.SourceAddress; + ObjectAddress = originalResourceNode.ObjectAddress; + ResourceHandle = originalResourceNode.ResourceHandle; GamePath = originalResourceNode.GamePath; PossibleGamePaths = originalResourceNode.PossibleGamePaths; FullPath = originalResourceNode.FullPath; + Length = originalResourceNode.Length; Internal = originalResourceNode.Internal; Children = originalResourceNode.Children; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 76d0c3f2..f14191c8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -57,7 +58,9 @@ public class ResourceTree var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); - } + } + + AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); @@ -95,7 +98,9 @@ public class ResourceTree subObjectNodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") : mdlNode); - } + } + + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; ++subObjectIndex; @@ -106,16 +111,25 @@ public class ResourceTree var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); - if (skeletonNode != null) - Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); - - var decalNode = context.CreateNodeFromTex(human->Decal); + var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); - var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); + var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + } + + private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + { + if (skeleton == null) + return; + + for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) + { + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (sklbNode != null) + nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index e9939496..d29916dd 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -12,7 +12,6 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { private readonly IDataManager _dataManager; - private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); public readonly List Characters; public readonly Dictionary CharactersById; @@ -27,10 +26,6 @@ internal class TreeBuildCache .ToDictionary(c => c.Key, c => c.First()); } - /// Try to read a material file from the given path and cache it on success. - public MtrlFile? ReadMaterial(FullPath path) - => ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes)); - /// Try to read a shpk file from the given path and cache it on success. public ShpkFile? ReadShaderPackage(FullPath path) => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 4d8c77a7..0d87215f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -128,7 +128,7 @@ public class ResourceTreeViewer if (debugMode) ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}"); } ImGui.TableNextColumn();