mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Merge branch 'master' into AeAstralis/tagging
This commit is contained in:
commit
312fc1df9a
15 changed files with 319 additions and 182 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit 79ffdd69a28141a1ac93daa24d76573b2fa0d71e
|
Subproject commit 34921fd2c5a9aff5d34aef664bdb78331e8b9436
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733
|
Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319
|
||||||
|
|
@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.P
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
|
/// <seealso cref="Interop.Services.ShaderReplacementFixer.OnMtrlShpkLoaded"/>
|
||||||
SkinFixer = 0,
|
ShaderReplacementFixer = 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,10 +214,18 @@ public class MeshExporter
|
||||||
var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count);
|
var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count);
|
||||||
shapeNames.Add(shape.ShapeName);
|
shapeNames.Add(shape.ShapeName);
|
||||||
|
|
||||||
foreach (var shapeValue in shapeValues)
|
foreach (var (shapeValue, shapeValueIndex) in shapeValues.WithIndex())
|
||||||
{
|
{
|
||||||
|
var gltfIndex = gltfIndices[shapeValue.BaseIndicesIndex - indexBase];
|
||||||
|
|
||||||
|
if (gltfIndex == -1)
|
||||||
|
{
|
||||||
|
_notifier.Warning($"{name}: Shape {shape.ShapeName} mapping {shapeValueIndex} targets a degenerate triangle, ignoring.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
morphBuilder.SetVertex(
|
morphBuilder.SetVertex(
|
||||||
primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(),
|
primitiveVertices[gltfIndex].GetGeometry(),
|
||||||
vertices[shapeValue.ReplacingVertexIndex].GetGeometry()
|
vertices[shapeValue.ReplacingVertexIndex].GetGeometry()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||||
StartIndex = 0,
|
StartIndex = 0,
|
||||||
IndexCount = (uint)_indices.Count,
|
IndexCount = (uint)_indices.Count,
|
||||||
|
|
||||||
// TODO: import material names
|
|
||||||
MaterialIndex = 0,
|
MaterialIndex = 0,
|
||||||
SubMeshIndex = 0,
|
SubMeshIndex = 0,
|
||||||
SubMeshCount = (ushort)_subMeshes.Count,
|
SubMeshCount = (ushort)_subMeshes.Count,
|
||||||
|
|
@ -167,7 +166,7 @@ public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||||
// And finally, merge in the sub-mesh struct itself.
|
// And finally, merge in the sub-mesh struct itself.
|
||||||
_subMeshes.Add(subMesh.SubMeshStruct with
|
_subMeshes.Add(subMesh.SubMeshStruct with
|
||||||
{
|
{
|
||||||
IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
|
IndexOffset = (uint)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
|
||||||
AttributeIndexMask = Utility.GetMergedAttributeMask(
|
AttributeIndexMask = Utility.GetMergedAttributeMask(
|
||||||
subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes),
|
subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Lumina.Data.Parsing;
|
using Lumina.Data.Parsing;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models.Import;
|
namespace Penumbra.Import.Models.Import;
|
||||||
|
|
||||||
|
|
@ -43,15 +44,15 @@ public static class Utility
|
||||||
throw notifier.Exception(
|
throw notifier.Exception(
|
||||||
$"""
|
$"""
|
||||||
All sub-meshes of a mesh must have equivalent vertex declarations.
|
All sub-meshes of a mesh must have equivalent vertex declarations.
|
||||||
Current: {FormatVertexDeclaration(current)}
|
Current: {FormatVertexDeclaration(current)}
|
||||||
New: {FormatVertexDeclaration(@new)}
|
New: {FormatVertexDeclaration(@new)}
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration)
|
private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration)
|
||||||
=> string.Join(", ",
|
=> string.Join(", ",
|
||||||
vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})"));
|
vertexDeclaration.VertexElements.Select(element => $"{(MdlFile.VertexUsage)element.Usage} ({(MdlFile.VertexType)element.Type}@{element.Stream}:{element.Offset})"));
|
||||||
|
|
||||||
private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b)
|
private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr<ResourceHa
|
||||||
/// <seealso cref="PathResolving.SubfileHelper"/>
|
/// <seealso cref="PathResolving.SubfileHelper"/>
|
||||||
SubfileHelper,
|
SubfileHelper,
|
||||||
|
|
||||||
/// <seealso cref="SkinFixer"/>
|
/// <seealso cref="ShaderReplacementFixer"/>
|
||||||
SkinFixer,
|
ShaderReplacementFixer,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceHandleDestructor(HookManager hooks)
|
public ResourceHandleDestructor(HookManager hooks)
|
||||||
|
|
|
||||||
71
Penumbra/Interop/Services/ModelRenderer.cs
Normal file
71
Penumbra/Interop/Services/ModelRenderer.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Services;
|
||||||
|
|
||||||
|
// TODO ClientStructs-ify (https://github.com/aers/FFXIVClientStructs/pull/817)
|
||||||
|
public unsafe class ModelRenderer : IDisposable
|
||||||
|
{
|
||||||
|
// Will be Manager.Instance()->ModelRenderer.CharacterGlassShaderPackage in CS
|
||||||
|
private const nint ModelRendererOffset = 0x13660;
|
||||||
|
private const nint CharacterGlassShaderPackageOffset = 0xD0;
|
||||||
|
|
||||||
|
/// <summary> A static pointer to the Render::Manager address. </summary>
|
||||||
|
[Signature(Sigs.RenderManager, ScanType = ScanType.StaticAddress)]
|
||||||
|
private readonly nint* _renderManagerAddress = null;
|
||||||
|
|
||||||
|
public bool Ready { get; private set; }
|
||||||
|
|
||||||
|
public ShaderPackageResourceHandle** CharacterGlassShaderPackage
|
||||||
|
=> *_renderManagerAddress == 0
|
||||||
|
? null
|
||||||
|
: (ShaderPackageResourceHandle**)(*_renderManagerAddress + ModelRendererOffset + CharacterGlassShaderPackageOffset).ToPointer();
|
||||||
|
|
||||||
|
public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; }
|
||||||
|
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
|
||||||
|
public ModelRenderer(IFramework framework, IGameInteropProvider interop)
|
||||||
|
{
|
||||||
|
interop.InitializeFromAttributes(this);
|
||||||
|
_framework = framework;
|
||||||
|
LoadDefaultResources(null!);
|
||||||
|
if (!Ready)
|
||||||
|
_framework.Update += LoadDefaultResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> We store the default data of the resources so we can always restore them. </summary>
|
||||||
|
private void LoadDefaultResources(object _)
|
||||||
|
{
|
||||||
|
if (*_renderManagerAddress == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var anyMissing = false;
|
||||||
|
|
||||||
|
if (DefaultCharacterGlassShaderPackage == null)
|
||||||
|
{
|
||||||
|
DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage;
|
||||||
|
anyMissing |= DefaultCharacterGlassShaderPackage == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyMissing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Ready = true;
|
||||||
|
_framework.Update -= LoadDefaultResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Return all relevant resources to the default resource. </summary>
|
||||||
|
public void ResetAll()
|
||||||
|
{
|
||||||
|
if (!Ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
*CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> ResetAll();
|
||||||
|
}
|
||||||
197
Penumbra/Interop/Services/ShaderReplacementFixer.cs
Normal file
197
Penumbra/Interop/Services/ShaderReplacementFixer.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using Penumbra.Communication;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.Hooks.Resources;
|
||||||
|
using Penumbra.Services;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Services;
|
||||||
|
|
||||||
|
public sealed unsafe class ShaderReplacementFixer : IDisposable
|
||||||
|
{
|
||||||
|
public static ReadOnlySpan<byte> SkinShpkName
|
||||||
|
=> "skin.shpk"u8;
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> CharacterGlassShpkName
|
||||||
|
=> "characterglass.shpk"u8;
|
||||||
|
|
||||||
|
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
|
||||||
|
private readonly nint* _humanVTable = null!;
|
||||||
|
|
||||||
|
private delegate nint CharacterBaseOnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param);
|
||||||
|
private delegate nint ModelRendererOnRenderMaterialDelegate(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
private struct OnRenderMaterialParams
|
||||||
|
{
|
||||||
|
[FieldOffset(0x0)]
|
||||||
|
public Model* Model;
|
||||||
|
|
||||||
|
[FieldOffset(0x8)]
|
||||||
|
public uint MaterialIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Hook<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook;
|
||||||
|
|
||||||
|
[Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))]
|
||||||
|
private readonly Hook<ModelRendererOnRenderMaterialDelegate> _modelRendererOnRenderMaterialHook = null!;
|
||||||
|
|
||||||
|
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
private readonly CharacterUtility _utility;
|
||||||
|
private readonly ModelRenderer _modelRenderer;
|
||||||
|
|
||||||
|
// MaterialResourceHandle set
|
||||||
|
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
|
||||||
|
private readonly ConcurrentSet<nint> _moddedCharacterGlassShpkMaterials = new();
|
||||||
|
|
||||||
|
private readonly object _skinLock = new();
|
||||||
|
private readonly object _characterGlassLock = new();
|
||||||
|
|
||||||
|
// ConcurrentDictionary.Count uses a lock in its current implementation.
|
||||||
|
private int _moddedSkinShpkCount;
|
||||||
|
private int _moddedCharacterGlassShpkCount;
|
||||||
|
private ulong _skinSlowPathCallDelta;
|
||||||
|
private ulong _characterGlassSlowPathCallDelta;
|
||||||
|
|
||||||
|
public bool Enabled { get; internal set; } = true;
|
||||||
|
|
||||||
|
public int ModdedSkinShpkCount
|
||||||
|
=> _moddedSkinShpkCount;
|
||||||
|
|
||||||
|
public int ModdedCharacterGlassShpkCount
|
||||||
|
=> _moddedCharacterGlassShpkCount;
|
||||||
|
|
||||||
|
public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer,
|
||||||
|
CommunicatorService communicator, IGameInteropProvider interop)
|
||||||
|
{
|
||||||
|
interop.InitializeFromAttributes(this);
|
||||||
|
_resourceHandleDestructor = resourceHandleDestructor;
|
||||||
|
_utility = utility;
|
||||||
|
_modelRenderer = modelRenderer;
|
||||||
|
_communicator = communicator;
|
||||||
|
_humanOnRenderMaterialHook = interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
||||||
|
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer);
|
||||||
|
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
|
||||||
|
_humanOnRenderMaterialHook.Enable();
|
||||||
|
_modelRendererOnRenderMaterialHook.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_modelRendererOnRenderMaterialHook.Dispose();
|
||||||
|
_humanOnRenderMaterialHook.Dispose();
|
||||||
|
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
||||||
|
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
||||||
|
_moddedCharacterGlassShpkMaterials.Clear();
|
||||||
|
_moddedSkinShpkMaterials.Clear();
|
||||||
|
_moddedCharacterGlassShpkCount = 0;
|
||||||
|
_moddedSkinShpkCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas()
|
||||||
|
=> (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0));
|
||||||
|
|
||||||
|
private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan<byte> shpkName)
|
||||||
|
{
|
||||||
|
if (mtrlResource == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
|
||||||
|
{
|
||||||
|
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
|
||||||
|
var shpk = mtrl->ShaderPackageResourceHandle;
|
||||||
|
if (shpk == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var shpkName = mtrl->ShpkNameSpan;
|
||||||
|
|
||||||
|
if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource)
|
||||||
|
{
|
||||||
|
if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle))
|
||||||
|
Interlocked.Increment(ref _moddedSkinShpkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage)
|
||||||
|
{
|
||||||
|
if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle))
|
||||||
|
Interlocked.Increment(ref _moddedCharacterGlassShpkCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResourceHandleDestructor(Structs.ResourceHandle* handle)
|
||||||
|
{
|
||||||
|
if (_moddedSkinShpkMaterials.TryRemove((nint)handle))
|
||||||
|
Interlocked.Decrement(ref _moddedSkinShpkCount);
|
||||||
|
|
||||||
|
if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle))
|
||||||
|
Interlocked.Decrement(ref _moddedCharacterGlassShpkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param)
|
||||||
|
{
|
||||||
|
// If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all.
|
||||||
|
if (!Enabled || _moddedSkinShpkCount == 0)
|
||||||
|
return _humanOnRenderMaterialHook.Original(human, param);
|
||||||
|
|
||||||
|
var material = param->Model->Materials[param->MaterialIndex];
|
||||||
|
var mtrlResource = material->MaterialResourceHandle;
|
||||||
|
if (!IsMaterialWithShpk(mtrlResource, SkinShpkName))
|
||||||
|
return _humanOnRenderMaterialHook.Original(human, param);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _skinSlowPathCallDelta);
|
||||||
|
|
||||||
|
// Performance considerations:
|
||||||
|
// - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ;
|
||||||
|
// - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ;
|
||||||
|
// - Swapping path is taken up to hundreds of times a frame.
|
||||||
|
// At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible.
|
||||||
|
lock (_skinLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle;
|
||||||
|
return _humanOnRenderMaterialHook.Original(human, param);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nint ModelRendererOnRenderMaterialDetour(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex)
|
||||||
|
{
|
||||||
|
|
||||||
|
// If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all.
|
||||||
|
if (!Enabled || _moddedCharacterGlassShpkCount == 0)
|
||||||
|
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
|
||||||
|
|
||||||
|
var mtrlResource = material->MaterialResourceHandle;
|
||||||
|
if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName))
|
||||||
|
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _characterGlassSlowPathCallDelta);
|
||||||
|
|
||||||
|
// Same performance considerations as above.
|
||||||
|
lock (_characterGlassLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
*_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle;
|
||||||
|
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
*_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
|
||||||
using OtterGui.Classes;
|
|
||||||
using Penumbra.Communication;
|
|
||||||
using Penumbra.GameData;
|
|
||||||
using Penumbra.Interop.Hooks.Resources;
|
|
||||||
using Penumbra.Services;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Services;
|
|
||||||
|
|
||||||
public sealed unsafe class SkinFixer : IDisposable
|
|
||||||
{
|
|
||||||
public static ReadOnlySpan<byte> SkinShpkName
|
|
||||||
=> "skin.shpk"u8;
|
|
||||||
|
|
||||||
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
|
|
||||||
private readonly nint* _humanVTable = null!;
|
|
||||||
|
|
||||||
private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
private struct OnRenderMaterialParams
|
|
||||||
{
|
|
||||||
[FieldOffset(0x0)]
|
|
||||||
public Model* Model;
|
|
||||||
|
|
||||||
[FieldOffset(0x8)]
|
|
||||||
public uint MaterialIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Hook<OnRenderMaterialDelegate> _onRenderMaterialHook;
|
|
||||||
|
|
||||||
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
|
||||||
private readonly CommunicatorService _communicator;
|
|
||||||
private readonly CharacterUtility _utility;
|
|
||||||
|
|
||||||
// MaterialResourceHandle set
|
|
||||||
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
|
|
||||||
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
// ConcurrentDictionary.Count uses a lock in its current implementation.
|
|
||||||
private int _moddedSkinShpkCount;
|
|
||||||
private ulong _slowPathCallDelta;
|
|
||||||
|
|
||||||
public bool Enabled { get; internal set; } = true;
|
|
||||||
|
|
||||||
public int ModdedSkinShpkCount
|
|
||||||
=> _moddedSkinShpkCount;
|
|
||||||
|
|
||||||
public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator,
|
|
||||||
IGameInteropProvider interop)
|
|
||||||
{
|
|
||||||
interop.InitializeFromAttributes(this);
|
|
||||||
_resourceHandleDestructor = resourceHandleDestructor;
|
|
||||||
_utility = utility;
|
|
||||||
_communicator = communicator;
|
|
||||||
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
|
||||||
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer);
|
|
||||||
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer);
|
|
||||||
_onRenderMaterialHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_onRenderMaterialHook.Dispose();
|
|
||||||
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
|
||||||
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
|
||||||
_moddedSkinShpkMaterials.Clear();
|
|
||||||
_moddedSkinShpkCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ulong GetAndResetSlowPathCallDelta()
|
|
||||||
=> Interlocked.Exchange(ref _slowPathCallDelta, 0);
|
|
||||||
|
|
||||||
private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource)
|
|
||||||
{
|
|
||||||
if (mtrlResource == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName);
|
|
||||||
return SkinShpkName.SequenceEqual(shpkName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
|
|
||||||
{
|
|
||||||
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
|
|
||||||
var shpk = mtrl->ShaderPackageResourceHandle;
|
|
||||||
if (shpk == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsSkinMaterial(mtrl) || (nint)shpk == _utility.DefaultSkinShpkResource)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle))
|
|
||||||
Interlocked.Increment(ref _moddedSkinShpkCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnResourceHandleDestructor(Structs.ResourceHandle* handle)
|
|
||||||
{
|
|
||||||
if (_moddedSkinShpkMaterials.TryRemove((nint)handle))
|
|
||||||
Interlocked.Decrement(ref _moddedSkinShpkCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param)
|
|
||||||
{
|
|
||||||
// If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all.
|
|
||||||
if (!Enabled || _moddedSkinShpkCount == 0)
|
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
|
||||||
|
|
||||||
var material = param->Model->Materials[param->MaterialIndex];
|
|
||||||
var mtrlResource = material->MaterialResourceHandle;
|
|
||||||
if (!IsSkinMaterial(mtrlResource))
|
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _slowPathCallDelta);
|
|
||||||
|
|
||||||
// Performance considerations:
|
|
||||||
// - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ;
|
|
||||||
// - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ;
|
|
||||||
// - Swapping path is taken up to hundreds of times a frame.
|
|
||||||
// At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible.
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle;
|
|
||||||
return _onRenderMaterialHook.Original(human, param);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
|
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
|
||||||
_collectionManager.Caches.CreateNecessaryCaches();
|
_collectionManager.Caches.CreateNecessaryCaches();
|
||||||
_services.GetService<PathResolver>();
|
_services.GetService<PathResolver>();
|
||||||
_services.GetService<SkinFixer>();
|
_services.GetService<ShaderReplacementFixer>();
|
||||||
|
|
||||||
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ public static class ServiceManagerA
|
||||||
return new CutsceneResolver(cutsceneService.GetParentIndex);
|
return new CutsceneResolver(cutsceneService.GetParentIndex);
|
||||||
})
|
})
|
||||||
.AddSingleton<CharacterUtility>()
|
.AddSingleton<CharacterUtility>()
|
||||||
|
.AddSingleton<ModelRenderer>()
|
||||||
.AddSingleton<ResourceManagerService>()
|
.AddSingleton<ResourceManagerService>()
|
||||||
.AddSingleton<ResourceService>()
|
.AddSingleton<ResourceService>()
|
||||||
.AddSingleton<FileReadService>()
|
.AddSingleton<FileReadService>()
|
||||||
|
|
@ -133,7 +134,7 @@ public static class ServiceManagerA
|
||||||
.AddSingleton<ResourceWatcher>()
|
.AddSingleton<ResourceWatcher>()
|
||||||
.AddSingleton<ResourceTreeFactory>()
|
.AddSingleton<ResourceTreeFactory>()
|
||||||
.AddSingleton<MetaFileManager>()
|
.AddSingleton<MetaFileManager>()
|
||||||
.AddSingleton<SkinFixer>();
|
.AddSingleton<ShaderReplacementFixer>();
|
||||||
|
|
||||||
private static ServiceManager AddResolvers(this ServiceManager services)
|
private static ServiceManager AddResolvers(this ServiceManager services)
|
||||||
=> services.AddSingleton<CollectionResolver>()
|
=> services.AddSingleton<CollectionResolver>()
|
||||||
|
|
|
||||||
|
|
@ -220,24 +220,9 @@ public partial class ModEditWindow
|
||||||
/// <param name="source"> Model to copy element ids from. </param>
|
/// <param name="source"> Model to copy element ids from. </param>
|
||||||
private static void MergeElementIds(MdlFile target, MdlFile source)
|
private static void MergeElementIds(MdlFile target, MdlFile source)
|
||||||
{
|
{
|
||||||
var elementIds = new List<MdlStructs.ElementIdStruct>();
|
// This is overly simplistic, but effectively reproduces what TT did, sort of.
|
||||||
|
// TODO: Get a better idea of what these values represent. `ParentBoneName`, if it is a pointer into the bone array, does not seem to be _bounded_ by the bone array length, at least in the model. I'm guessing it _may_ be pointing into a .sklb instead? (i.e. the weapon's skeleton). EID stuff in general needs more work.
|
||||||
foreach (var sourceElement in source.ElementIds)
|
target.ElementIds = [.. source.ElementIds];
|
||||||
{
|
|
||||||
var sourceBone = source.Bones[sourceElement.ParentBoneName];
|
|
||||||
var targetIndex = target.Bones.IndexOf(sourceBone);
|
|
||||||
// Given that there's no means of authoring these at the moment, this should probably remain a hard error.
|
|
||||||
if (targetIndex == -1)
|
|
||||||
throw new Exception(
|
|
||||||
$"Failed to merge element IDs. Original model contains element IDs targeting bone {sourceBone}, which is not present on the imported model.");
|
|
||||||
|
|
||||||
elementIds.Add(sourceElement with
|
|
||||||
{
|
|
||||||
ParentBoneName = (uint)targetIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
target.ElementIds = [.. elementIds];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeginIo()
|
private void BeginIo()
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ public class DebugTab : Window, ITab
|
||||||
private readonly ImportPopup _importPopup;
|
private readonly ImportPopup _importPopup;
|
||||||
private readonly FrameworkManager _framework;
|
private readonly FrameworkManager _framework;
|
||||||
private readonly TextureManager _textureManager;
|
private readonly TextureManager _textureManager;
|
||||||
private readonly SkinFixer _skinFixer;
|
private readonly ShaderReplacementFixer _shaderReplacementFixer;
|
||||||
private readonly RedrawService _redraws;
|
private readonly RedrawService _redraws;
|
||||||
private readonly DictEmote _emotes;
|
private readonly DictEmote _emotes;
|
||||||
private readonly Diagnostics _diagnostics;
|
private readonly Diagnostics _diagnostics;
|
||||||
|
|
@ -99,7 +99,7 @@ public class DebugTab : Window, ITab
|
||||||
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
||||||
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
||||||
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
|
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
|
||||||
TextureManager textureManager, SkinFixer skinFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester)
|
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester)
|
||||||
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
|
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
|
||||||
{
|
{
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
@ -130,7 +130,7 @@ public class DebugTab : Window, ITab
|
||||||
_importPopup = importPopup;
|
_importPopup = importPopup;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_textureManager = textureManager;
|
_textureManager = textureManager;
|
||||||
_skinFixer = skinFixer;
|
_shaderReplacementFixer = shaderReplacementFixer;
|
||||||
_redraws = redraws;
|
_redraws = redraws;
|
||||||
_emotes = emotes;
|
_emotes = emotes;
|
||||||
_diagnostics = diagnostics;
|
_diagnostics = diagnostics;
|
||||||
|
|
@ -702,20 +702,25 @@ public class DebugTab : Window, ITab
|
||||||
if (!ImGui.CollapsingHeader("Character Utility"))
|
if (!ImGui.CollapsingHeader("Character Utility"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var enableSkinFixer = _skinFixer.Enabled;
|
var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled;
|
||||||
if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer))
|
if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer))
|
||||||
_skinFixer.Enabled = enableSkinFixer;
|
_shaderReplacementFixer.Enabled = enableShaderReplacementFixer;
|
||||||
|
|
||||||
if (enableSkinFixer)
|
if (enableShaderReplacementFixer)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
||||||
|
var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas();
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}");
|
ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}");
|
ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_shaderReplacementFixer.ModdedSkinShpkCount}");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted($"characterglass.shpk: {_shaderReplacementFixer.ModdedCharacterGlassShpkCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
|
using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Components;
|
using Dalamud.Interface.Components;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
@ -228,7 +229,15 @@ public class SettingsTab : ITab
|
||||||
|
|
||||||
using var group = ImRaii.Group();
|
using var group = ImRaii.Group();
|
||||||
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
||||||
var save = ImGui.InputText("##rootDirectory", ref _newModDirectory, RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue);
|
bool save;
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid))
|
||||||
|
{
|
||||||
|
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder)
|
||||||
|
.Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid);
|
||||||
|
save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory,
|
||||||
|
RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue);
|
||||||
|
}
|
||||||
|
|
||||||
var selected = ImGui.IsItemActive();
|
var selected = ImGui.IsItemActive();
|
||||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0));
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0));
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue