diff --git a/OtterGui b/OtterGui index 8c7a309d..86ec4d72 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428 +Subproject commit 86ec4d72c9c9ed57aa7be4a7d0c81069c5b94ad7 diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index 48ba86a5..69d84ce2 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -16,9 +16,6 @@ public sealed class CreatedCharacterBase : EventWrapper Api = int.MinValue, - - /// - SkinFixer = 0, } public CreatedCharacterBase() diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs new file mode 100644 index 00000000..4b5600c9 --- /dev/null +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -0,0 +1,24 @@ +using System; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Parameter is the material resource handle for which the shader package has been loaded. +/// Parameter is the associated game object. +/// +public sealed class MtrlShpkLoaded : EventWrapper, MtrlShpkLoaded.Priority> +{ + public enum Priority + { + /// + SkinFixer = 0, + } + + public MtrlShpkLoaded() + : base(nameof(MtrlShpkLoaded)) + { } + + public void Invoke(nint mtrlResourceHandle, nint gameObject) + => Invoke(this, mtrlResourceHandle, gameObject); +} diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 05f42220..c0b8c5e3 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -24,22 +25,24 @@ namespace Penumbra.Interop.PathResolving; /// public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> { - private readonly PerformanceTracker _performance; - private readonly ResourceLoader _loader; - private readonly GameEventManager _events; + private readonly PerformanceTracker _performance; + private readonly ResourceLoader _loader; + private readonly GameEventManager _events; + private readonly CommunicatorService _communicator; private readonly ThreadLocal _mtrlData = new(() => ResolveData.Invalid); private readonly ThreadLocal _avfxData = new(() => ResolveData.Invalid); private readonly ConcurrentDictionary _subFileCollection = new(); - public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events) + public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator) { SignatureHelper.Initialise(this); - _performance = performance; - _loader = loader; - _events = events; + _performance = performance; + _loader = loader; + _events = events; + _communicator = communicator; _loadMtrlShpkHook.Enable(); _loadMtrlTexHook.Enable(); @@ -118,7 +121,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollectionFileType) { case ResourceType.Mtrl: - case ResourceType.Avfx: + case ResourceType.Avfx: if (handle->FileSize == 0) _subFileCollection[(nint)handle] = resolveData; @@ -151,9 +154,11 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection SkinShpkName + => "skin.shpk"u8; [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] private readonly nint* _humanVTable = null!; @@ -45,107 +35,85 @@ public sealed unsafe class SkinFixer : IDisposable private readonly GameEventManager _gameEvents; private readonly CommunicatorService _communicator; - private readonly ResourceLoader _resources; private readonly CharacterUtility _utility; - - // CharacterBase to ShpkHandle - private readonly ConcurrentDictionary _skinShpks = new(); + + // MaterialResourceHandle set + private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); private readonly object _lock = new(); - private int _moddedSkinShpkCount = 0; - private ulong _slowPathCallDelta = 0; + // 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(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) + public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator) { SignatureHelper.Initialise(this); _gameEvents = gameEvents; - _resources = resources; _utility = utility; - _communicator = communicator; + _communicator = communicator; _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); - _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); - _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer); + _gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor; _onRenderMaterialHook.Enable(); } public void Dispose() { _onRenderMaterialHook.Dispose(); - _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); - _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; - foreach (var skinShpk in _skinShpks.Values) - skinShpk.Dispose(); - _skinShpks.Clear(); + _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor; + _moddedSkinShpkMaterials.Clear(); _moddedSkinShpkCount = 0; } public ulong GetAndResetSlowPathCallDelta() => Interlocked.Exchange(ref _slowPathCallDelta, 0); - private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) + private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource) { - if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) - return; + if (mtrlResource == null) + return false; - Task.Run(() => - { - var skinShpk = SafeResourceHandle.CreateInvalid(); - try - { - var data = collection.ToResolveData(gameObject); - if (data.Valid) - { - var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); - skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); - } - } - catch (Exception e) - { - Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - } - - if (!skinShpk.IsInvalid) - { - if (_skinShpks.TryAdd(drawObject, skinShpk)) - { - if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); - } - else - { - skinShpk.Dispose(); - } - } - }); + var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString); + return SkinShpkName.SequenceEqual(shpkName); } - private void OnCharacterBaseDestructor(nint characterBase) + private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) { - if (!_skinShpks.Remove(characterBase, out var skinShpk)) + var mtrl = (Structs.MtrlResource*)mtrlResourceHandle; + var shpk = mtrl->ShpkResourceHandle; + if (shpk == null) return; - var handle = skinShpk.ResourceHandle; - skinShpk.Dispose(); - if ((nint)handle != _utility.DefaultSkinShpkResource) + 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 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) - return _onRenderMaterialHook!.Original(human, param); + if (!Enabled || _moddedSkinShpkCount == 0) + return _onRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; - var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; - if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) - return _onRenderMaterialHook!.Original(human, param); + var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle; + if (!IsSkinMaterial(mtrlResource)) + return _onRenderMaterialHook.Original(human, param); Interlocked.Increment(ref _slowPathCallDelta); @@ -158,8 +126,8 @@ public sealed unsafe class SkinFixer : IDisposable { try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; - return _onRenderMaterialHook!.Original(human, param); + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle; + return _onRenderMaterialHook.Original(human, param); } finally { diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 728b391c..97340f6b 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -24,6 +24,9 @@ public class CommunicatorService : IDisposable /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); + /// + public readonly MtrlShpkLoaded MtrlShpkLoaded = new(); + /// public readonly ModDataChanged ModDataChanged = new(); @@ -75,6 +78,7 @@ public class CommunicatorService : IDisposable TemporaryGlobalModChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); + MtrlShpkLoaded.Dispose(); ModDataChanged.Dispose(); ModOptionChanged.Dispose(); ModDiscoveryStarted.Dispose(); diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index c24d64fa..d02da883 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -603,7 +603,7 @@ public class DebugTab : Window, ITab ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); } using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,