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,