From 1284037554fdeebd4e21d7a3ed846629b1c43e6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 14:39:03 +0200 Subject: [PATCH 1/2] Fix some hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 10 ++-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 1 - Penumbra/Interop/Hooks/Meta/GmpHook.cs | 47 ++++--------------- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 ++-- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 ++-- .../Interop/Hooks/Objects/CopyCharacter.cs | 5 +- .../Hooks/Objects/CreateCharacterBase.cs | 6 +-- .../Interop/Structs/CharacterUtilityData.cs | 10 ++-- Penumbra/Interop/Structs/MetaIndex.cs | 13 +++-- 11 files changed, 40 insertions(+), 75 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19923f8d..27d15145 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 +Subproject commit 27d15145567c11e2bb1902857f8db25f02189390 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index ed4eb669..9dd8d74f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -4,11 +4,11 @@ public static class HookSettings { public const bool AllHooks = true; - public const bool ObjectHooks = false && AllHooks; + public const bool ObjectHooks = true && AllHooks; public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = false && AllHooks; - public const bool MetaEntryHooks = false && AllHooks; - public const bool MetaParentHooks = false && AllHooks; + public const bool ResourceHooks = true && AllHooks; + public const bool MetaEntryHooks = true && AllHooks; + public const bool MetaParentHooks = true && AllHooks; public const bool VfxIdentificationHooks = false && AllHooks; - public const bool PostProcessingHooks = false && AllHooks; + public const bool PostProcessingHooks = true && AllHooks; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 3767c4a2..e90674a8 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 329a8beb..12b221d9 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Data.Parsing.Uld; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -8,12 +10,10 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class GmpHook : FastHook, IDisposable { - public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + public delegate ulong Delegate(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId); private readonly MetaState _metaState; - private static readonly Finalizer StablePointer = new(); - public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; @@ -21,50 +21,21 @@ public unsafe class GmpHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - /// - /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . - /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, - /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. - /// - private nint Detour(nint gmpResource, uint dividedHeadId) + private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) { - nint ret; + ulong ret; if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) - { - if (entry.Entry.Enabled) - { - *StablePointer.Pointer = entry.Entry.Value; - // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. - // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); - } - else - { - ret = nint.Zero; - } - } + ret = (*outputEntry) = entry.Entry.Enabled ? entry.Entry.Value : 0ul; else - { - ret = Task.Result.Original(gmpResource, dividedHeadId); - } + ret = Task.Result.Original(characterUtility, outputEntry, setId); - Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + Penumbra.Log.Excessive( + $"[GetGmpFlags] Invoked on 0x{(ulong)characterUtility:X} for {setId} with 0x{(ulong)outputEntry:X} (={*outputEntry:X}), returned {ret:X10}."); return ret; } - /// Allocate and clean up our single stable ulong pointer. - private class Finalizer - { - public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); - - ~Finalizer() - { - Marshal.FreeHGlobal((nint)Pointer); - } - } - public void Dispose() => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 79e7f6a6..c1803745 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index eb8a8a37..e08dc393 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -10,8 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class RspBustHook : FastHook, IDisposable { - public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, - byte bustSize); + public delegate float* Delegate(nint cmpResource, float* storage, SubRace race, byte gender, byte bodyType, byte bustSize); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -24,7 +23,7 @@ public unsafe class RspBustHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) { if (gender == 0) { @@ -38,7 +37,6 @@ public unsafe class RspBustHook : FastHook, IDisposable if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); @@ -58,11 +56,11 @@ public unsafe class RspBustHook : FastHook, IDisposable } else { - ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + ret = Task.Result.Original(cmpResource, storage, clan, gender, bodyType, bustSize); } Penumbra.Log.Excessive( - $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index f8f9e51e..20e3c939 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public class RspHeightHook : FastHook, IDisposable { - public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + public delegate float Delegate(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -23,7 +23,7 @@ public class RspHeightHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) { float scale; if (bodyType < 2 @@ -36,7 +36,6 @@ public class RspHeightHook : FastHook, IDisposable if (height > 100) height = 0; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); @@ -68,11 +67,11 @@ public class RspHeightHook : FastHook, IDisposable } else { - scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + scale = Task.Result.Original(cmpResource, clan, gender, bodyType, height); } Penumbra.Log.Excessive( - $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {height}, returned {scale}."); return scale; } diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 663209ae..d81043c8 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -9,7 +9,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr + /// CutsceneService = 0, } @@ -38,8 +38,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtrOwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); Invoke(character, source); return _task.Result.Original(target, source, unk); diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 56b3d853..f00a9984 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -10,7 +10,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// MetaState = 0, } @@ -64,10 +64,10 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MetaState = 0, } } diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 22150cc1..d33da477 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -6,16 +6,16 @@ namespace Penumbra.Interop.Structs; public unsafe struct CharacterUtilityData { public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - public const int IndexSkinShpk = 76; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexSkinShpk = 83; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 87; + public const int TotalNumResources = 89; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) @@ -36,7 +36,7 @@ public unsafe struct CharacterUtilityData 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, - //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs index 65302264..2ec5fce4 100644 --- a/Penumbra/Interop/Structs/MetaIndex.cs +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -4,6 +4,7 @@ namespace Penumbra.Interop.Structs; public enum MetaIndex : int { Eqp = 0, + Evp = 1, Gmp = 2, Eqdp0101 = 3, @@ -21,9 +22,8 @@ public enum MetaIndex : int Eqdp1301, Eqdp1401, Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, + Eqdp1601, + Eqdp1701, Eqdp1801, Eqdp0104, Eqdp0204, @@ -51,9 +51,8 @@ public enum MetaIndex : int Eqdp1301Acc, Eqdp1401Acc, Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1601Acc, + Eqdp1701Acc, Eqdp1801Acc, Eqdp0104Acc, Eqdp0204Acc, @@ -66,7 +65,7 @@ public enum MetaIndex : int Eqdp9104Acc, Eqdp9204Acc, - HumanCmp = 64, + HumanCmp = 71, FaceEst, HairEst, HeadEst, From 41d271213efb97c34d173f3759d110780bd81d5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 5 Jul 2024 23:59:22 +0200 Subject: [PATCH 2/2] Update ShaderReplacementFixer for 7.0 --- .../PostProcessing/ShaderReplacementFixer.cs | 250 ++++++++++++++---- Penumbra/Interop/Services/ModelRenderer.cs | 82 +++++- Penumbra/UI/Tabs/Debug/DebugTab.cs | 97 +++++-- 3 files changed, 359 insertions(+), 70 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b27ca4c5..b87d33ef 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -19,9 +19,24 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan IrisShpkName + => "iris.shpk"u8; + public static ReadOnlySpan CharacterGlassShpkName => "characterglass.shpk"u8; + public static ReadOnlySpan CharacterTransparencyShpkName + => "charactertransparency.shpk"u8; + + public static ReadOnlySpan CharacterTattooShpkName + => "charactertattoo.shpk"u8; + + public static ReadOnlySpan CharacterOcclusionShpkName + => "characterocclusion.shpk"u8; + + public static ReadOnlySpan HairMaskShpkName + => "hairmask.shpk"u8; + private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, @@ -36,26 +51,36 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; - // MaterialResourceHandle set - private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); - private readonly ConcurrentSet _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; + private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _irisState; + private readonly ModdedShaderPackageState _characterGlassState; + private readonly ModdedShaderPackageState _characterTransparencyState; + private readonly ModdedShaderPackageState _characterTattooState; + private readonly ModdedShaderPackageState _characterOcclusionState; + private readonly ModdedShaderPackageState _hairMaskState; public bool Enabled { get; internal set; } = true; - public int ModdedSkinShpkCount - => _moddedSkinShpkCount; + public uint ModdedSkinShpkCount + => _skinState.MaterialCount; - public int ModdedCharacterGlassShpkCount - => _moddedCharacterGlassShpkCount; + public uint ModdedIrisShpkCount + => _irisState.MaterialCount; + + public uint ModdedCharacterGlassShpkCount + => _characterGlassState.MaterialCount; + + public uint ModdedCharacterTransparencyShpkCount + => _characterTransparencyState.MaterialCount; + + public uint ModdedCharacterTattooShpkCount + => _characterTattooState.MaterialCount; + + public uint ModdedCharacterOcclusionShpkCount + => _characterOcclusionState.MaterialCount; + + public uint ModdedHairMaskShpkCount + => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) @@ -64,7 +89,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; - _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[62], + + _skinState = new( + () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _irisState = new(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new(() => _modelRenderer.CharacterTransparencyShaderPackage, () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new(() => _modelRenderer.CharacterTattooShaderPackage, () => _modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new(() => _modelRenderer.CharacterOcclusionShaderPackage, () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + _hairMaskState = new(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + + _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; @@ -78,14 +114,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _humanOnRenderMaterialHook.Dispose(); _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); - _moddedCharacterGlassShpkMaterials.Clear(); - _moddedSkinShpkMaterials.Clear(); - _moddedCharacterGlassShpkCount = 0; - _moddedSkinShpkCount = 0; + _hairMaskState.ClearMaterials(); + _characterOcclusionState.ClearMaterials(); + _characterTattooState.ClearMaterials(); + _characterTransparencyState.ClearMaterials(); + _characterGlassState.ClearMaterials(); + _irisState.ClearMaterials(); + _skinState.ClearMaterials(); } - public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() - => (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0)); + public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() + => (_skinState.GetAndResetSlowPathCallDelta(), + _irisState.GetAndResetSlowPathCallDelta(), + _characterGlassState.GetAndResetSlowPathCallDelta(), + _characterTransparencyState.GetAndResetSlowPathCallDelta(), + _characterTattooState.GetAndResetSlowPathCallDelta(), + _characterOcclusionState.GetAndResetSlowPathCallDelta(), + _hairMaskState.GetAndResetSlowPathCallDelta()); private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) { @@ -102,54 +147,99 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; + var shpkName = mtrl->ShpkNameSpan; + var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); - 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); + if (shpkState != null && shpk != shpkState.DefaultShaderPackage) + shpkState.TryAddMaterial(mtrlResourceHandle); } private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedSkinShpkCount); - - if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); + _skinState.TryRemoveMaterial(handle); + _irisState.TryRemoveMaterial(handle); + _characterGlassState.TryRemoveMaterial(handle); + _characterTransparencyState.TryRemoveMaterial(handle); + _characterTattooState.TryRemoveMaterial(handle); + _characterOcclusionState.TryRemoveMaterial(handle); + _hairMaskState.TryRemoveMaterial(handle); } + private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + { + if (SkinShpkName.SequenceEqual(shpkName)) + return _skinState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHuman() + => _skinState.MaterialCount; + + private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterGlassShpkName.SequenceEqual(shpkName)) + return _characterGlassState; + + if (CharacterTransparencyShpkName.SequenceEqual(shpkName)) + return _characterTransparencyState; + + if (CharacterTattooShpkName.SequenceEqual(shpkName)) + return _characterTattooState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (HairMaskShpkName.SequenceEqual(shpkName)) + return _hairMaskState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRenderer() + => _irisState.MaterialCount + _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + 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 || _moddedSkinShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForHuman() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, SkinShpkName)) + var shpkState = GetStateForHuman(mtrlResource); + if (shpkState == null) return _humanOnRenderMaterialHook.Original(human, param); - Interlocked.Increment(ref _skinSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // 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 frame rate or CPU usage, but the swapping path shall still be avoided as much as possible. - lock (_skinLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _humanOnRenderMaterialHook.Original(human, param); } finally { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + *shpkReference = shpkState.DefaultShaderPackage; } } } @@ -158,27 +248,91 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic 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 || _moddedCharacterGlassShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForModelRenderer() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName)) + var shpkState = GetStateForModelRenderer(mtrlResource); + if (shpkState == null) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); - Interlocked.Increment(ref _characterGlassSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // Same performance considerations as above. - lock (_characterGlassLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - *_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); } finally { - *_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage; + *shpkReference = shpkState.DefaultShaderPackage; } } } + + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) + { + // MaterialResourceHandle set + private readonly ConcurrentSet _materials = new(); + + // ConcurrentDictionary.Count uses a lock in its current implementation. + private uint _materialCount = 0; + + private ulong _slowPathCallDelta = 0; + + public uint MaterialCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => _materialCount; + } + + public ShaderPackageResourceHandle** ShaderPackageReference + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => referenceGetter(); + } + + public ShaderPackageResourceHandle* DefaultShaderPackage + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => defaultGetter(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryAddMaterial(nint mtrlResourceHandle) + { + if (_materials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryRemoveMaterial(Structs.ResourceHandle* handle) + { + if (_materials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void ClearMaterials() + { + _materials.Clear(); + _materialCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void IncrementSlowPathCallDelta() + => Interlocked.Increment(ref _slowPathCallDelta); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + } + + private delegate ShaderPackageResourceHandle* DefaultShaderPackageGetter(); + + private delegate ShaderPackageResourceHandle** ShaderPackageReferenceGetter(); } diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index b268b395..10f3977f 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -9,6 +9,13 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } + public ShaderPackageResourceHandle** IrisShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + }; + public ShaderPackageResourceHandle** CharacterGlassShaderPackage => Manager.Instance() switch { @@ -16,8 +23,46 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, }; + public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterTattooShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + }; + + public ShaderPackageResourceHandle** HairMaskShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + }; + + public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterTransparencyShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterTattooShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterOcclusionShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultHairMaskShaderPackage { get; private set; } + private readonly IFramework _framework; public ModelRenderer(IFramework framework) @@ -36,12 +81,42 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var anyMissing = false; + if (DefaultIrisShaderPackage == null) + { + DefaultIrisShaderPackage = *IrisShaderPackage; + anyMissing |= DefaultIrisShaderPackage == null; + } + if (DefaultCharacterGlassShaderPackage == null) { DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; anyMissing |= DefaultCharacterGlassShaderPackage == null; } + if (DefaultCharacterTransparencyShaderPackage == null) + { + DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + } + + if (DefaultCharacterTattooShaderPackage == null) + { + DefaultCharacterTattooShaderPackage = *CharacterTattooShaderPackage; + anyMissing |= DefaultCharacterTattooShaderPackage == null; + } + + if (DefaultCharacterOcclusionShaderPackage == null) + { + DefaultCharacterOcclusionShaderPackage = *CharacterOcclusionShaderPackage; + anyMissing |= DefaultCharacterOcclusionShaderPackage == null; + } + + if (DefaultHairMaskShaderPackage == null) + { + DefaultHairMaskShaderPackage = *HairMaskShaderPackage; + anyMissing |= DefaultHairMaskShaderPackage == null; + } + if (anyMissing) return; @@ -55,7 +130,12 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (!Ready) return; - *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *HairMaskShaderPackage = DefaultHairMaskShaderPackage; + *CharacterOcclusionShaderPackage = DefaultCharacterOcclusionShaderPackage; + *CharacterTattooShaderPackage = DefaultCharacterTattooShaderPackage; + *CharacterTransparencyShaderPackage = DefaultCharacterTransparencyShaderPackage; + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *IrisShaderPackage = DefaultIrisShaderPackage; } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a03f384..0c2581bf 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -177,6 +177,8 @@ public class DebugTab : Window, ITab, IUiService ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawShaderReplacementFixer(); + ImGui.NewLine(); DrawData(); ImGui.NewLine(); DrawResourceProblems(); @@ -711,27 +713,6 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; - if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) - _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - - if (enableShaderReplacementFixer) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); - ImGui.SameLine(); - ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}"); - ImGui.SameLine(); - ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}"); - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - 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, -Vector2.UnitX); if (!table) @@ -786,6 +767,80 @@ public class DebugTab : Window, ITab, IUiService } } + private void DrawShaderReplacementFixer() + { + if (!ImGui.CollapsingHeader("Shader Replacement Fixer")) + return; + + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; + + if (!enableShaderReplacementFixer) + return; + + using var table = Table("##ShaderReplacementFixer", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); + + ImGui.TableSetupColumn("Shader Package Name", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Materials with Modded ShPk", ImGuiTableColumnFlags.WidthStretch, 0.2f); + 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(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + 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.TableNextColumn(); + ImGui.TextUnformatted("characterocclusion.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterOcclusionShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("hairmask.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + } + /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() {