From fb58a9c27194d2107cd926d3a31f5a8d4600a1d4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 8 Aug 2024 23:19:18 +0200 Subject: [PATCH] Add/improve ShaderReplacementFixer hooks --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../PostProcessing/HumanSetupScalingHook.cs | 55 ++++ .../PostProcessing/PreBoneDeformerReplacer.cs | 49 +-- .../PostProcessing/ShaderReplacementFixer.cs | 284 ++++++++++++++++-- Penumbra/Interop/Services/CharacterUtility.cs | 32 +- Penumbra/Interop/Services/ModelRenderer.cs | 44 +-- .../Interop/Structs/CharacterUtilityData.cs | 26 +- .../Interop/Structs/ModelRendererStructs.cs | 35 +++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 62 ++-- 10 files changed, 478 insertions(+), 113 deletions(-) create mode 100644 Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs create mode 100644 Penumbra/Interop/Structs/ModelRendererStructs.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ac9d9c78..2fd5aa44 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947 +Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 0bc55dc5..a1dd374f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -80,6 +80,8 @@ public class HookOverrides public bool HumanCreateDeformer; public bool HumanOnRenderMaterial; public bool ModelRendererOnRenderMaterial; + public bool ModelRendererUnkFunc; + public bool PrepareColorTable; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs new file mode 100644 index 00000000..5783c099 --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +// TODO: "SetupScaling" does not seem to only set up scaling -> find a better name? +public unsafe class HumanSetupScalingHook : FastHook +{ + private const int ReplacementCapacity = 2; + + public event EventDelegate? SetupReplacements; + + public HumanSetupScalingHook(HookManager hooks, CharacterBaseVTables vTables) + { + Task = hooks.CreateHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, + !HookOverrides.Instance.PostProcessing.HumanSetupScaling); + } + + private void Detour(CharacterBase* drawObject, uint slotIndex) + { + Span replacements = stackalloc Replacement[ReplacementCapacity]; + var numReplacements = 0; + IDisposable? pbdDisposable = null; + object? shpkLock = null; + var releaseLock = false; + + try + { + SetupReplacements?.Invoke(drawObject, slotIndex, replacements, ref numReplacements, ref pbdDisposable, ref shpkLock); + if (shpkLock != null) + { + Monitor.Enter(shpkLock); + releaseLock = true; + } + for (var i = 0; i < numReplacements; ++i) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToSet; + Task.Result.Original(drawObject, slotIndex); + } + finally + { + for (var i = numReplacements; i-- > 0;) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToRestore; + if (releaseLock) + Monitor.Exit(shpkLock!); + pbdDisposable?.Dispose(); + } + } + + public delegate void Delegate(CharacterBase* drawObject, uint slotIndex); + + public delegate void EventDelegate(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, + ref IDisposable? pbdDisposable, ref object? shpkLock); + + public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 1aa09d7f..9273a2cb 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -18,27 +18,26 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public static readonly Utf8GamePath PreBoneDeformerPath = Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; - // Approximate name guesses. - private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); - private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + // Approximate name guess. + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); - private readonly Hook _humanSetupScalingHook; private readonly Hook _humanCreateDeformerHook; - private readonly CharacterUtility _utility; - private readonly CollectionResolver _collectionResolver; - private readonly ResourceLoader _resourceLoader; - private readonly IFramework _framework; + private readonly CharacterUtility _utility; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceLoader _resourceLoader; + private readonly IFramework _framework; + private readonly HumanSetupScalingHook _humanSetupScalingHook; public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, - HookManager hooks, IFramework framework, CharacterBaseVTables vTables) + HookManager hooks, IFramework framework, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, - !HookOverrides.Instance.PostProcessing.HumanSetupScaling).Result; + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = humanSetupScalingHook; + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } @@ -46,7 +45,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public void Dispose() { _humanCreateDeformerHook.Dispose(); - _humanSetupScalingHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; } private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) @@ -58,22 +57,24 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } - private void SetupScaling(CharacterBase* drawObject, uint slotIndex) + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( - $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHSSReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { - if (!preBoneDeformer.IsInvalid) - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; - _humanSetupScalingHook.Original(drawObject, slotIndex); + pbdDisposable = preBoneDeformer; + replacements[numReplacements++] = new((nint)(&_utility.Address->HumanPbdResource), (nint)preBoneDeformer.ResourceHandle, + _utility.DefaultHumanPbdResource); } - finally + catch { - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + preBoneDeformer.Dispose(); + throw; } } diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index d02e18bb..20db7e25 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -7,6 +8,8 @@ using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; using Penumbra.Services; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; @@ -19,6 +22,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan CharacterStockingsShpkName + => "characterstockings.shpk"u8; + + public static ReadOnlySpan CharacterLegacyShpkName + => "characterlegacy.shpk"u8; + public static ReadOnlySpan IrisShpkName => "iris.shpk"u8; @@ -42,16 +51,26 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); + private delegate void ModelRendererUnkFuncDelegate(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, + uint unk3, uint unk4, uint unk5); + private readonly Hook _humanOnRenderMaterialHook; private readonly Hook _modelRendererOnRenderMaterialHook; + private readonly Hook _modelRendererUnkFuncHook; + + private readonly Hook _prepareColorTableHook; + private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; + private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _characterStockingsState; + private readonly ModdedShaderPackageState _characterLegacyState; private readonly ModdedShaderPackageState _irisState; private readonly ModdedShaderPackageState _characterGlassState; private readonly ModdedShaderPackageState _characterTransparencyState; @@ -64,6 +83,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public uint ModdedSkinShpkCount => _skinState.MaterialCount; + public uint ModdedCharacterStockingsShpkCount + => _characterStockingsState.MaterialCount; + + public uint ModdedCharacterLegacyShpkCount + => _characterLegacyState.MaterialCount; + public uint ModdedIrisShpkCount => _irisState.MaterialCount; @@ -83,16 +108,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) + CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _characterStockingsState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + _characterLegacyState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); @@ -105,33 +137,50 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState = new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; + _modelRendererUnkFuncHook = hooks.CreateHook("ModelRenderer.UnkFunc", + Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour, + !HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result; + _prepareColorTableHook = hooks.CreateHook("MaterialResourceHandle.PrepareColorTable", + Sigs.PrepareColorSet, PrepareColorTableDetour, + !HookOverrides.Instance.PostProcessing.PrepareColorTable).Result; + _communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } public void Dispose() { + _prepareColorTableHook.Dispose(); + _modelRendererUnkFuncHook.Dispose(); _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); + _hairMaskState.ClearMaterials(); _characterOcclusionState.ClearMaterials(); _characterTattooState.ClearMaterials(); _characterTransparencyState.ClearMaterials(); _characterGlassState.ClearMaterials(); _irisState.ClearMaterials(); + _characterLegacyState.ClearMaterials(); + _characterStockingsState.ClearMaterials(); _skinState.ClearMaterials(); } - public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong - HairMask) GetAndResetSlowPathCallDeltas() + public (ulong Skin, ulong CharacterStockings, ulong CharacterLegacy, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong + CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() => (_skinState.GetAndResetSlowPathCallDelta(), + _characterStockingsState.GetAndResetSlowPathCallDelta(), + _characterLegacyState.GetAndResetSlowPathCallDelta(), _irisState.GetAndResetSlowPathCallDelta(), _characterGlassState.GetAndResetSlowPathCallDelta(), _characterTransparencyState.GetAndResetSlowPathCallDelta(), @@ -155,7 +204,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return; var shpkName = mtrl->ShpkNameSpan; - var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); + var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) + ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); if (shpkState != null && shpk != shpkState.DefaultShaderPackage) shpkState.TryAddMaterial(mtrlResourceHandle); @@ -164,6 +214,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { _skinState.TryRemoveMaterial(handle); + _characterStockingsState.TryRemoveMaterial(handle); + _characterLegacyState.TryRemoveMaterial(handle); _irisState.TryRemoveMaterial(handle); _characterGlassState.TryRemoveMaterial(handle); _characterTransparencyState.TryRemoveMaterial(handle); @@ -172,10 +224,25 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState.TryRemoveMaterial(handle); } - private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) + { + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHumanSetup() + => _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) { if (SkinShpkName.SequenceEqual(shpkName)) return _skinState; @@ -184,17 +251,14 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForHuman() + private uint GetTotalMaterialCountForHumanRender() => _skinState.MaterialCount; - private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan shpkName) { - if (IrisShpkName.SequenceEqual(shpkName)) - return _irisState; - if (CharacterGlassShpkName.SequenceEqual(shpkName)) return _characterGlassState; @@ -204,9 +268,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (CharacterTattooShpkName.SequenceEqual(shpkName)) return _characterTattooState; - if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) - return _characterOcclusionState; - if (HairMaskShpkName.SequenceEqual(shpkName)) return _hairMaskState; @@ -214,24 +275,93 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForModelRenderer() - => _irisState.MaterialCount - + _characterGlassState.MaterialCount + private uint GetTotalMaterialCountForModelRendererRender() + => _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount - + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRendererUnk() + => _irisState.MaterialCount + + _characterOcclusionState.MaterialCount + + _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForColorTable(ReadOnlySpan shpkName) + { + if (CharacterLegacyShpkName.SequenceEqual(shpkName)) + return _characterLegacyState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForColorTable() + => _characterLegacyState.MaterialCount; + + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) + { + // If we don't have any on-screen instances of modded characterstockings.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForHumanSetup() == 0) + return; + + var model = drawObject->Models[slotIndex]; + if (model == null) + return; + MaterialResourceHandle* mtrlResource = null; + ModdedShaderPackageState? shpkState = null; + foreach (var material in model->MaterialsSpan) + { + if (material.Value == null) + continue; + + mtrlResource = material.Value->MaterialResourceHandle; + shpkState = GetStateForHumanSetup(mtrlResource); + // Despite this function being called with what designates a model (and therefore potentially many materials), + // we currently don't need to handle more than one modded ShPk. + if (shpkState != null) + break; + } + if (shpkState == null || shpkState.MaterialCount == 0) + return; + + shpkState.IncrementSlowPathCallDelta(); + + // This is less performance-critical than the others, as this is called by the game only on draw object creation and slot update. + // There are still thread safety concerns as it might be called in other threads by plugins. + shpkLock = shpkState; + replacements[numReplacements++] = new((nint)shpkState.ShaderPackageReference, (nint)mtrlResource->ShaderPackageResourceHandle, + (nint)shpkState.DefaultShaderPackage); + } + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.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 || GetTotalMaterialCountForHuman() == 0) + if (!Enabled || GetTotalMaterialCountForHumanRender() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForHuman(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForHumanRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _humanOnRenderMaterialHook.Original(human, param); shpkState.IncrementSlowPathCallDelta(); @@ -259,18 +389,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* 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 || GetTotalMaterialCountForModelRenderer() == 0) + // If we don't have any on-screen instances of modded characterglass.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererRender() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForModelRenderer(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForModelRendererRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); shpkState.IncrementSlowPathCallDelta(); - // Same performance considerations as above. + // Same performance considerations as OnRenderHumanMaterial. lock (shpkState) { var shpkReference = shpkState.ShaderPackageReference; @@ -286,6 +416,102 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } } + private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3, + uint unk4, uint unk5) + { + // If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + var mtrlResource = GetMaterialResourceHandle(unkPayload); + var shpkState = GetStateForModelRendererUnk(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as OnRenderHumanMaterial. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = mtrlResource->ShaderPackageResourceHandle; + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + + private MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) + { + // TODO ClientStructs-ify + var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var materialIndex = *(ushort*)(unkPointer + 8); + var material = unkPayload->Params->Model->Materials[materialIndex]; + if (material == null) + return null; + + var mtrlResource = material->MaterialResourceHandle; + if (mtrlResource == null) + return null; + + if (mtrlResource->ShaderPackageResourceHandle == null) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); + return null; + } + + if (mtrlResource->ShaderPackageResourceHandle->ShaderPackage != unkPayload->ShaderWrapper->ShaderPackage) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); + return null; + } + + return mtrlResource; + } + + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) + { + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForColorTable() == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var material = thisPtr->Material; + if (material == null) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan); + if (shpkState == null || shpkState.MaterialCount == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as HumanSetupScalingDetour. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = thisPtr->ShaderPackageResourceHandle; + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) { // MaterialResourceHandle set diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 532dc823..4ab156a9 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -28,10 +28,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService public bool Ready { get; private set; } public event Action LoadingFinished; - public nint DefaultHumanPbdResource { get; private set; } - public nint DefaultTransparentResource { get; private set; } - public nint DefaultDecalResource { get; private set; } - public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultHumanPbdResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultCharacterStockingsShpkResource { get; private set; } + public nint DefaultCharacterLegacyShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -108,6 +110,18 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService anyMissing |= DefaultSkinShpkResource == nint.Zero; } + if (DefaultCharacterStockingsShpkResource == nint.Zero) + { + DefaultCharacterStockingsShpkResource = (nint)Address->CharacterStockingsShpkResource; + anyMissing |= DefaultCharacterStockingsShpkResource == nint.Zero; + } + + if (DefaultCharacterLegacyShpkResource == nint.Zero) + { + DefaultCharacterLegacyShpkResource = (nint)Address->CharacterLegacyShpkResource; + anyMissing |= DefaultCharacterLegacyShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -122,10 +136,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService if (!Ready) return; - Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; - Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; - Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; - Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; + Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; + Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->CharacterStockingsShpkResource = (ResourceHandle*)DefaultCharacterStockingsShpkResource; + Address->CharacterLegacyShpkResource = (ResourceHandle*)DefaultCharacterLegacyShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 10f3977f..5e2cd1fb 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; +using ModelRendererData = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; namespace Penumbra.Interop.Services; @@ -9,46 +10,53 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } - public ShaderPackageResourceHandle** IrisShaderPackage + public ModelRendererData* Address => Manager.Instance() switch { null => null, - var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + var renderManager => &renderManager->ModelRenderer, + }; + + public ShaderPackageResourceHandle** IrisShaderPackage + => Address switch + { + null => null, + var data => &data->IrisShaderPackage, }; public ShaderPackageResourceHandle** CharacterGlassShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, + null => null, + var data => &data->CharacterGlassShaderPackage, }; public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + null => null, + var data => &data->CharacterTransparencyShaderPackage, }; public ShaderPackageResourceHandle** CharacterTattooShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + null => null, + var data => &data->CharacterTattooShaderPackage, }; public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + null => null, + var data => &data->CharacterOcclusionShaderPackage, }; public ShaderPackageResourceHandle** HairMaskShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + null => null, + var data => &data->HairMaskShaderPackage, }; public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } @@ -96,7 +104,7 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (DefaultCharacterTransparencyShaderPackage == null) { DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; - anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; } if (DefaultCharacterTattooShaderPackage == null) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 7595353f..8543466d 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,15 +5,17 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexTileOrbArrayTex = 81; - public const int IndexTileNormArrayTex = 82; - public const int IndexSkinShpk = 83; - public const int IndexGudStm = 94; - public const int IndexLegacyStm = 95; - public const int IndexSphereDArrayTex = 96; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexCharacterStockingsShpk = 84; + public const int IndexCharacterLegacyShpk = 85; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -111,6 +113,12 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexCharacterStockingsShpk * 8)] + public ResourceHandle* CharacterStockingsShpkResource; + + [FieldOffset(8 + IndexCharacterLegacyShpk * 8)] + public ResourceHandle* CharacterLegacyShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] public ResourceHandle* GudStmResource; diff --git a/Penumbra/Interop/Structs/ModelRendererStructs.cs b/Penumbra/Interop/Structs/ModelRendererStructs.cs new file mode 100644 index 00000000..551a32e3 --- /dev/null +++ b/Penumbra/Interop/Structs/ModelRendererStructs.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Structs; + +public static unsafe class ModelRendererStructs +{ + [StructLayout(LayoutKind.Explicit, Size = 0x28)] + public struct UnkShaderWrapper + { + [FieldOffset(0)] + public void* Vtbl; + + [FieldOffset(8)] + public ShaderPackage* ShaderPackage; + } + + // Unknown size, this is allocated on FUN_1404446c0's stack (E8 ?? ?? ?? ?? FF C3 41 3B DE 72 ?? 48 C7 85) + [StructLayout(LayoutKind.Explicit)] + public struct UnkPayload + { + [FieldOffset(0)] + public ModelRenderer.OnRenderModelParams* Params; + + [FieldOffset(8)] + public ModelResourceHandle* ModelResourceHandle; + + [FieldOffset(0x10)] + public UnkShaderWrapper* ShaderWrapper; + + [FieldOffset(0x1C)] + public ushort UnkIndex; + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 7dae19c8..5b82a523 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -833,20 +833,6 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableHeadersRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("skin.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("iris.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); - ImGui.TableNextColumn(); ImGui.TextUnformatted("characterglass.shpk"); ImGui.TableNextColumn(); @@ -855,18 +841,11 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TextUnformatted("characterlegacy.shpk"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterLegacyShpkCount}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertattoo.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterLegacy}"); ImGui.TableNextColumn(); ImGui.TextUnformatted("characterocclusion.shpk"); @@ -875,12 +854,47 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterstockings.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterStockingsShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterStockings}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertattoo.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); + ImGui.TableNextColumn(); ImGui.TextUnformatted("hairmask.shpk"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("iris.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("skin.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); } /// Draw information about the resident resource files.