Skip to content

Commit 29c93f4

Browse files
Exter-NOttermandias
authored andcommitted
Add characterglass.shpk to the skin.shpk fixer
1 parent da423b7 commit 29c93f4

File tree

9 files changed

+290
-155
lines changed

9 files changed

+290
-155
lines changed

Penumbra/Communication/MtrlShpkLoaded.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.P
1010
{
1111
public enum Priority
1212
{
13-
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
14-
SkinFixer = 0,
13+
/// <seealso cref="Interop.Services.ShaderReplacementFixer.OnMtrlShpkLoaded"/>
14+
ShaderReplacementFixer = 0,
1515
}
1616
}

Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public enum Priority
1313
/// <seealso cref="PathResolving.SubfileHelper"/>
1414
SubfileHelper,
1515

16-
/// <seealso cref="SkinFixer"/>
17-
SkinFixer,
16+
/// <seealso cref="ShaderReplacementFixer"/>
17+
ShaderReplacementFixer,
1818
}
1919

2020
public ResourceHandleDestructor(HookManager hooks)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Dalamud.Plugin.Services;
2+
using Dalamud.Utility.Signatures;
3+
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
4+
using Penumbra.GameData;
5+
6+
namespace Penumbra.Interop.Services;
7+
8+
// TODO ClientStructs-ify (https://github.com/aers/FFXIVClientStructs/pull/817)
9+
public unsafe class ModelRenderer : IDisposable
10+
{
11+
// Will be Manager.Instance()->ModelRenderer.CharacterGlassShaderPackage in CS
12+
private const nint ModelRendererOffset = 0x13660;
13+
private const nint CharacterGlassShaderPackageOffset = 0xD0;
14+
15+
/// <summary> A static pointer to the Render::Manager address. </summary>
16+
[Signature(Sigs.RenderManager, ScanType = ScanType.StaticAddress)]
17+
private readonly nint* _renderManagerAddress = null;
18+
19+
public bool Ready { get; private set; }
20+
21+
public ShaderPackageResourceHandle** CharacterGlassShaderPackage
22+
=> *_renderManagerAddress == 0
23+
? null
24+
: (ShaderPackageResourceHandle**)(*_renderManagerAddress + ModelRendererOffset + CharacterGlassShaderPackageOffset).ToPointer();
25+
26+
public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; }
27+
28+
private readonly IFramework _framework;
29+
30+
public ModelRenderer(IFramework framework, IGameInteropProvider interop)
31+
{
32+
interop.InitializeFromAttributes(this);
33+
_framework = framework;
34+
LoadDefaultResources(null!);
35+
if (!Ready)
36+
_framework.Update += LoadDefaultResources;
37+
}
38+
39+
/// <summary> We store the default data of the resources so we can always restore them. </summary>
40+
private void LoadDefaultResources(object _)
41+
{
42+
if (*_renderManagerAddress == 0)
43+
return;
44+
45+
var anyMissing = false;
46+
47+
if (DefaultCharacterGlassShaderPackage == null)
48+
{
49+
DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage;
50+
anyMissing |= DefaultCharacterGlassShaderPackage == null;
51+
}
52+
53+
if (anyMissing)
54+
return;
55+
56+
Ready = true;
57+
_framework.Update -= LoadDefaultResources;
58+
}
59+
60+
/// <summary> Return all relevant resources to the default resource. </summary>
61+
public void ResetAll()
62+
{
63+
if (!Ready)
64+
return;
65+
66+
*CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage;
67+
}
68+
69+
public void Dispose()
70+
=> ResetAll();
71+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using Dalamud.Hooking;
2+
using Dalamud.Plugin.Services;
3+
using Dalamud.Utility.Signatures;
4+
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
5+
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
6+
using OtterGui.Classes;
7+
using Penumbra.Communication;
8+
using Penumbra.GameData;
9+
using Penumbra.Interop.Hooks.Resources;
10+
using Penumbra.Services;
11+
12+
namespace Penumbra.Interop.Services;
13+
14+
public sealed unsafe class ShaderReplacementFixer : IDisposable
15+
{
16+
public static ReadOnlySpan<byte> SkinShpkName
17+
=> "skin.shpk"u8;
18+
19+
public static ReadOnlySpan<byte> CharacterGlassShpkName
20+
=> "characterglass.shpk"u8;
21+
22+
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
23+
private readonly nint* _humanVTable = null!;
24+
25+
private delegate nint CharacterBaseOnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param);
26+
private delegate nint ModelRendererOnRenderMaterialDelegate(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex);
27+
28+
[StructLayout(LayoutKind.Explicit)]
29+
private struct OnRenderMaterialParams
30+
{
31+
[FieldOffset(0x0)]
32+
public Model* Model;
33+
34+
[FieldOffset(0x8)]
35+
public uint MaterialIndex;
36+
}
37+
38+
private readonly Hook<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook;
39+
40+
[Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))]
41+
private readonly Hook<ModelRendererOnRenderMaterialDelegate> _modelRendererOnRenderMaterialHook = null!;
42+
43+
private readonly ResourceHandleDestructor _resourceHandleDestructor;
44+
private readonly CommunicatorService _communicator;
45+
private readonly CharacterUtility _utility;
46+
private readonly ModelRenderer _modelRenderer;
47+
48+
// MaterialResourceHandle set
49+
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
50+
private readonly ConcurrentSet<nint> _moddedCharacterGlassShpkMaterials = new();
51+
52+
private readonly object _skinLock = new();
53+
private readonly object _characterGlassLock = new();
54+
55+
// ConcurrentDictionary.Count uses a lock in its current implementation.
56+
private int _moddedSkinShpkCount;
57+
private int _moddedCharacterGlassShpkCount;
58+
private ulong _skinSlowPathCallDelta;
59+
private ulong _characterGlassSlowPathCallDelta;
60+
61+
public bool Enabled { get; internal set; } = true;
62+
63+
public int ModdedSkinShpkCount
64+
=> _moddedSkinShpkCount;
65+
66+
public int ModdedCharacterGlassShpkCount
67+
=> _moddedCharacterGlassShpkCount;
68+
69+
public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer,
70+
CommunicatorService communicator, IGameInteropProvider interop)
71+
{
72+
interop.InitializeFromAttributes(this);
73+
_resourceHandleDestructor = resourceHandleDestructor;
74+
_utility = utility;
75+
_modelRenderer = modelRenderer;
76+
_communicator = communicator;
77+
_humanOnRenderMaterialHook = interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
78+
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer);
79+
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
80+
_humanOnRenderMaterialHook.Enable();
81+
_modelRendererOnRenderMaterialHook.Enable();
82+
}
83+
84+
public void Dispose()
85+
{
86+
_modelRendererOnRenderMaterialHook.Dispose();
87+
_humanOnRenderMaterialHook.Dispose();
88+
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
89+
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
90+
_moddedCharacterGlassShpkMaterials.Clear();
91+
_moddedSkinShpkMaterials.Clear();
92+
_moddedCharacterGlassShpkCount = 0;
93+
_moddedSkinShpkCount = 0;
94+
}
95+
96+
public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas()
97+
=> (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0));
98+
99+
private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan<byte> shpkName)
100+
{
101+
if (mtrlResource == null)
102+
return false;
103+
104+
return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan);
105+
}
106+
107+
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
108+
{
109+
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
110+
var shpk = mtrl->ShaderPackageResourceHandle;
111+
if (shpk == null)
112+
return;
113+
114+
var shpkName = mtrl->ShpkNameSpan;
115+
116+
if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource)
117+
{
118+
if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle))
119+
Interlocked.Increment(ref _moddedSkinShpkCount);
120+
}
121+
122+
if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage)
123+
{
124+
if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle))
125+
Interlocked.Increment(ref _moddedCharacterGlassShpkCount);
126+
}
127+
}
128+
129+
private void OnResourceHandleDestructor(Structs.ResourceHandle* handle)
130+
{
131+
if (_moddedSkinShpkMaterials.TryRemove((nint)handle))
132+
Interlocked.Decrement(ref _moddedSkinShpkCount);
133+
134+
if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle))
135+
Interlocked.Decrement(ref _moddedCharacterGlassShpkCount);
136+
}
137+
138+
private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param)
139+
{
140+
// If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all.
141+
if (!Enabled || _moddedSkinShpkCount == 0)
142+
return _humanOnRenderMaterialHook.Original(human, param);
143+
144+
var material = param->Model->Materials[param->MaterialIndex];
145+
var mtrlResource = material->MaterialResourceHandle;
146+
if (!IsMaterialWithShpk(mtrlResource, SkinShpkName))
147+
return _humanOnRenderMaterialHook.Original(human, param);
148+
149+
Interlocked.Increment(ref _skinSlowPathCallDelta);
150+
151+
// Performance considerations:
152+
// - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ;
153+
// - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ;
154+
// - Swapping path is taken up to hundreds of times a frame.
155+
// 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.
156+
lock (_skinLock)
157+
{
158+
try
159+
{
160+
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle;
161+
return _humanOnRenderMaterialHook.Original(human, param);
162+
}
163+
finally
164+
{
165+
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource;
166+
}
167+
}
168+
}
169+
170+
private nint ModelRendererOnRenderMaterialDetour(nint modelRenderer, nint outFlags, nint param, Material* material, uint materialIndex)
171+
{
172+
173+
// If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all.
174+
if (!Enabled || _moddedCharacterGlassShpkCount == 0)
175+
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
176+
177+
var mtrlResource = material->MaterialResourceHandle;
178+
if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName))
179+
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
180+
181+
Interlocked.Increment(ref _characterGlassSlowPathCallDelta);
182+
183+
// Same performance considerations as above.
184+
lock (_characterGlassLock)
185+
{
186+
try
187+
{
188+
*_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle;
189+
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);
190+
}
191+
finally
192+
{
193+
*_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage;
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)