Add characterglass.shpk to the skin.shpk fixer

This commit is contained in:
Exter-N 2024-03-09 22:28:46 +01:00 committed by Ottermandias
parent da423b7464
commit 29c93f46a0
9 changed files with 290 additions and 155 deletions

@ -1 +1 @@
Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 Subproject commit c0c7eb0dedb32ea83b019626abba041e90a95319

View file

@ -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,
} }
} }

View file

@ -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)

View 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();
}

View 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;
}
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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.

View file

@ -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>()
@ -132,7 +133,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>()

View file

@ -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,