Add/improve ShaderReplacementFixer hooks

This commit is contained in:
Exter-N 2024-08-08 23:19:18 +02:00
parent d630a3dff4
commit fb58a9c271
10 changed files with 478 additions and 113 deletions

@ -1 +1 @@
Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947
Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff

View file

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

View file

@ -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<HumanSetupScalingHook.Delegate>
{
private const int ReplacementCapacity = 2;
public event EventDelegate? SetupReplacements;
public HumanSetupScalingHook(HookManager hooks, CharacterBaseVTables vTables)
{
Task = hooks.CreateHook<Delegate>("Human.SetupScaling", vTables.HumanVTable[58], Detour,
!HookOverrides.Instance.PostProcessing.HumanSetupScaling);
}
private void Detour(CharacterBase* drawObject, uint slotIndex)
{
Span<Replacement> 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<Replacement> replacements, ref int numReplacements,
ref IDisposable? pbdDisposable, ref object? shpkLock);
public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore);
}

View file

@ -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<CharacterBaseSetupScalingDelegate> _humanSetupScalingHook;
private readonly Hook<CharacterBaseCreateDeformerDelegate> _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<CharacterBaseSetupScalingDelegate>("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<CharacterBaseCreateDeformerDelegate>("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<HumanSetupScalingHook.Replacement> 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;
}
}

View file

@ -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<byte> SkinShpkName
=> "skin.shpk"u8;
public static ReadOnlySpan<byte> CharacterStockingsShpkName
=> "characterstockings.shpk"u8;
public static ReadOnlySpan<byte> CharacterLegacyShpkName
=> "characterlegacy.shpk"u8;
public static ReadOnlySpan<byte> 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<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook;
private readonly Hook<ModelRendererOnRenderMaterialDelegate> _modelRendererOnRenderMaterialHook;
private readonly Hook<ModelRendererUnkFuncDelegate> _modelRendererUnkFuncHook;
private readonly Hook<MaterialResourceHandle.Delegates.PrepareColorTable> _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<CharacterBaseOnRenderMaterialDelegate>("Human.OnRenderMaterial", vTables.HumanVTable[64],
OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result;
_modelRendererOnRenderMaterialHook = hooks.CreateHook<ModelRendererOnRenderMaterialDelegate>("ModelRenderer.OnRenderMaterial",
Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour,
!HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result;
_modelRendererUnkFuncHook = hooks.CreateHook<ModelRendererUnkFuncDelegate>("ModelRenderer.UnkFunc",
Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour,
!HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result;
_prepareColorTableHook = hooks.CreateHook<MaterialResourceHandle.Delegates.PrepareColorTable>("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<byte> shpkName)
private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan<byte> 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<byte> 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<byte> shpkName)
private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan<byte> 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<byte> 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<byte> 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<HumanSetupScalingHook.Replacement> 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

View file

@ -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; }
/// <summary>
/// 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()

View file

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

View file

@ -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<MetaIndex>()
.Zip(Enum.GetValues<MetaIndex>())
@ -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;

View file

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

View file

@ -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}");
}
/// <summary> Draw information about the resident resource files. </summary>