-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/skin-shpk-fixer'
- Loading branch information
Showing
12 changed files
with
357 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,30 @@ | ||
using System; | ||
using OtterGui.Classes; | ||
using Penumbra.Api; | ||
using Penumbra.Collections; | ||
|
||
namespace Penumbra.Communication; | ||
|
||
/// <summary> <list type="number"> | ||
/// <item>Parameter is the game object for which a draw object is created. </item> | ||
/// <item>Parameter is the name of the applied collection. </item> | ||
/// <item>Parameter is the applied collection. </item> | ||
/// <item>Parameter is the created draw object. </item> | ||
/// </list> </summary> | ||
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, string, nint>, CreatedCharacterBase.Priority> | ||
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, ModCollection, nint>, CreatedCharacterBase.Priority> | ||
{ | ||
public enum Priority | ||
{ | ||
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/> | ||
Api = 0, | ||
Api = int.MinValue, | ||
|
||
/// <seealso cref="Interop.Services.SkinFixer.OnCharacterBaseCreated"/> | ||
SkinFixer = 0, | ||
} | ||
|
||
public CreatedCharacterBase() | ||
: base(nameof(CreatedCharacterBase)) | ||
{ } | ||
|
||
public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) | ||
=> Invoke(this, gameObject, appliedCollectionName, drawObject); | ||
public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) | ||
=> Invoke(this, gameObject, appliedCollection, drawObject); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
using System; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; | ||
|
||
namespace Penumbra.Interop.SafeHandles; | ||
|
||
public unsafe class SafeResourceHandle : SafeHandle | ||
{ | ||
public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; | ||
|
||
public override bool IsInvalid => handle == 0; | ||
|
||
public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) | ||
{ | ||
if (incRef && !ownsHandle) | ||
throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); | ||
if (incRef && handle != null) | ||
handle->IncRef(); | ||
SetHandle((nint)handle); | ||
} | ||
|
||
public static SafeResourceHandle CreateInvalid() | ||
=> new(null, incRef: false); | ||
|
||
protected override bool ReleaseHandle() | ||
{ | ||
var handle = Interlocked.Exchange(ref this.handle, 0); | ||
if (handle != 0) | ||
((ResourceHandle*)handle)->DecRef(); | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Dalamud.Hooking; | ||
using Dalamud.Utility.Signatures; | ||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; | ||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; | ||
using FFXIVClientStructs.FFXIV.Client.System.Resource; | ||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; | ||
using Penumbra.Collections; | ||
using Penumbra.Communication; | ||
using Penumbra.GameData; | ||
using Penumbra.GameData.Enums; | ||
using Penumbra.Interop.ResourceLoading; | ||
using Penumbra.Interop.SafeHandles; | ||
using Penumbra.Services; | ||
using Penumbra.String.Classes; | ||
|
||
namespace Penumbra.Interop.Services; | ||
|
||
public sealed unsafe class SkinFixer : IDisposable | ||
{ | ||
public static readonly Utf8GamePath SkinShpkPath = | ||
Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; | ||
|
||
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] | ||
private readonly nint* _humanVTable = null!; | ||
|
||
private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); | ||
|
||
[StructLayout(LayoutKind.Explicit)] | ||
private struct OnRenderMaterialParams | ||
{ | ||
[FieldOffset(0x0)] | ||
public Model* Model; | ||
|
||
[FieldOffset(0x8)] | ||
public uint MaterialIndex; | ||
} | ||
|
||
private readonly Hook<OnRenderMaterialDelegate> _onRenderMaterialHook; | ||
|
||
private readonly GameEventManager _gameEvents; | ||
private readonly CommunicatorService _communicator; | ||
private readonly ResourceLoader _resources; | ||
private readonly CharacterUtility _utility; | ||
|
||
// CharacterBase to ShpkHandle | ||
private readonly ConcurrentDictionary<nint, SafeResourceHandle> _skinShpks = new(); | ||
|
||
private readonly object _lock = new(); | ||
|
||
private int _moddedSkinShpkCount = 0; | ||
private ulong _slowPathCallDelta = 0; | ||
|
||
public bool Enabled { get; internal set; } = true; | ||
|
||
public int ModdedSkinShpkCount | ||
=> _moddedSkinShpkCount; | ||
|
||
public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) | ||
{ | ||
SignatureHelper.Initialise(this); | ||
_gameEvents = gameEvents; | ||
_resources = resources; | ||
_utility = utility; | ||
_communicator = communicator; | ||
_onRenderMaterialHook = Hook<OnRenderMaterialDelegate>.FromAddress(_humanVTable[62], OnRenderHumanMaterial); | ||
_communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); | ||
_gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; | ||
_onRenderMaterialHook.Enable(); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_onRenderMaterialHook.Dispose(); | ||
_communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); | ||
_gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; | ||
foreach (var skinShpk in _skinShpks.Values) | ||
skinShpk.Dispose(); | ||
_skinShpks.Clear(); | ||
_moddedSkinShpkCount = 0; | ||
} | ||
|
||
public ulong GetAndResetSlowPathCallDelta() | ||
=> Interlocked.Exchange(ref _slowPathCallDelta, 0); | ||
|
||
private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) | ||
{ | ||
if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) | ||
return; | ||
|
||
Task.Run(() => | ||
{ | ||
var skinShpk = SafeResourceHandle.CreateInvalid(); | ||
try | ||
{ | ||
var data = collection.ToResolveData(gameObject); | ||
if (data.Valid) | ||
{ | ||
var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); | ||
skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); | ||
} | ||
if (!skinShpk.IsInvalid) | ||
{ | ||
if (_skinShpks.TryAdd(drawObject, skinShpk)) | ||
{ | ||
if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) | ||
Interlocked.Increment(ref _moddedSkinShpkCount); | ||
} | ||
else | ||
{ | ||
skinShpk.Dispose(); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
private void OnCharacterBaseDestructor(nint characterBase) | ||
{ | ||
if (!_skinShpks.Remove(characterBase, out var skinShpk)) | ||
return; | ||
|
||
var handle = skinShpk.ResourceHandle; | ||
skinShpk.Dispose(); | ||
if ((nint)handle != _utility.DefaultSkinShpkResource) | ||
Interlocked.Decrement(ref _moddedSkinShpkCount); | ||
} | ||
|
||
private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) | ||
{ | ||
// If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. | ||
if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) | ||
return _onRenderMaterialHook!.Original(human, param); | ||
|
||
var material = param->Model->Materials[param->MaterialIndex]; | ||
var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; | ||
if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) | ||
return _onRenderMaterialHook!.Original(human, param); | ||
|
||
Interlocked.Increment(ref _slowPathCallDelta); | ||
|
||
// Performance considerations: | ||
// - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; | ||
// - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; | ||
// - Swapping path is taken up to hundreds of times a frame. | ||
// At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. | ||
lock (_lock) | ||
{ | ||
try | ||
{ | ||
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; | ||
return _onRenderMaterialHook!.Original(human, param); | ||
} | ||
finally | ||
{ | ||
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.