From 29c93f46a0131ba28eca812697b3010b86e520db Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Mar 2024 22:28:46 +0100 Subject: [PATCH] Add characterglass.shpk to the skin.shpk fixer --- Penumbra.GameData | 2 +- Penumbra/Communication/MtrlShpkLoaded.cs | 4 +- .../Resources/ResourceHandleDestructor.cs | 4 +- Penumbra/Interop/Services/ModelRenderer.cs | 71 +++++++ .../Services/ShaderReplacementFixer.cs | 197 ++++++++++++++++++ Penumbra/Interop/Services/SkinFixer.cs | 139 ------------ Penumbra/Penumbra.cs | 2 +- Penumbra/Services/ServiceManagerA.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 23 +- 9 files changed, 290 insertions(+), 155 deletions(-) create mode 100644 Penumbra/Interop/Services/ModelRenderer.cs create mode 100644 Penumbra/Interop/Services/ShaderReplacementFixer.cs delete mode 100644 Penumbra/Interop/Services/SkinFixer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a7f6d86..c0c7eb0d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 +Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319 diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index bd560fd8..8aab0e0e 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper - SkinFixer = 0, + /// + ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 31387101..5ddb7eaa 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -13,8 +13,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr SubfileHelper, - /// - SkinFixer, + /// + ShaderReplacementFixer, } public ResourceHandleDestructor(HookManager hooks) diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs new file mode 100644 index 00000000..6a3bf776 --- /dev/null +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -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; + + /// A static pointer to the Render::Manager address. + [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; + } + + /// We store the default data of the resources so we can always restore them. + 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; + } + + /// Return all relevant resources to the default resource. + public void ResetAll() + { + if (!Ready) + return; + + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + } + + public void Dispose() + => ResetAll(); +} diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs new file mode 100644 index 00000000..e57fe313 --- /dev/null +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -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 SkinShpkName + => "skin.shpk"u8; + + public static ReadOnlySpan 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 _humanOnRenderMaterialHook; + + [Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))] + private readonly Hook _modelRendererOnRenderMaterialHook = null!; + + private readonly ResourceHandleDestructor _resourceHandleDestructor; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _utility; + private readonly ModelRenderer _modelRenderer; + + // MaterialResourceHandle set + private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); + private readonly ConcurrentSet _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(_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 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; + } + } + } +} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs deleted file mode 100644 index 21331916..00000000 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ /dev/null @@ -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 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 _onRenderMaterialHook; - - private readonly ResourceHandleDestructor _resourceHandleDestructor; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - - // MaterialResourceHandle set - private readonly ConcurrentSet _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(_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; - } - } - } -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 15b7ce56..67f523ba 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); + _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index f25aac7c..191d8d11 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -92,6 +92,7 @@ public static class ServiceManagerA return new CutsceneResolver(cutsceneService.GetParentIndex); }) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -132,7 +133,7 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 66b93b04..f4ddbe31 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -86,7 +86,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; - private readonly SkinFixer _skinFixer; + private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly RedrawService _redraws; private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; @@ -99,7 +99,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, 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) { IsOpen = true; @@ -130,7 +130,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; - _skinFixer = skinFixer; + _shaderReplacementFixer = shaderReplacementFixer; _redraws = redraws; _emotes = emotes; _diagnostics = diagnostics; @@ -702,20 +702,25 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableSkinFixer = _skinFixer.Enabled; - if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) - _skinFixer.Enabled = enableSkinFixer; + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - if (enableSkinFixer) + if (enableShaderReplacementFixer) { ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); 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.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); 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,