Skip to content

Commit

Permalink
Merge branch 'feature/skin-shpk-fixer'
Browse files Browse the repository at this point in the history
  • Loading branch information
Ottermandias committed Aug 30, 2023
2 parents 600f598 + 6d3e930 commit 5ba993c
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 57 deletions.
26 changes: 6 additions & 20 deletions Penumbra/Api/PenumbraApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,7 @@ public event CreatingCharacterBaseDelegate? CreatingCharacterBase
}
}

public event CreatedCharacterBaseDelegate? CreatedCharacterBase
{
add
{
if (value == null)
return;

CheckInitialized();
_communicator.CreatedCharacterBase.Subscribe(new Action<nint, string, nint>(value),
Communication.CreatedCharacterBase.Priority.Api);
}
remove
{
if (value == null)
return;

CheckInitialized();
_communicator.CreatedCharacterBase.Unsubscribe(new Action<nint, string, nint>(value));
}
}
public event CreatedCharacterBaseDelegate? CreatedCharacterBase;

public bool Valid
=> _lumina != null;
Expand Down Expand Up @@ -157,6 +138,7 @@ public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManage
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api);
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
}

public unsafe void Dispose()
Expand All @@ -167,6 +149,7 @@ public unsafe void Dispose()
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
_communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
_lumina = null;
_communicator = null!;
_modManager = null!;
Expand Down Expand Up @@ -1189,4 +1172,7 @@ private ActorIdentifier NameToIdentifier(string name, ushort worldId)

private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited)
=> ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited);

private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject)
=> CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject);
}
14 changes: 9 additions & 5 deletions Penumbra/Communication/CreatedCharacterBase.cs
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);
}
2 changes: 1 addition & 1 deletion Penumbra/Interop/PathResolving/MetaState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject)
_characterBaseCreateMetaChanges = DisposableContainer.Empty;
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, drawObject);
_lastCreatedCollection.ModCollection, drawObject);
_lastCreatedCollection = ResolveData.Invalid;
}

Expand Down
34 changes: 34 additions & 0 deletions Penumbra/Interop/SafeHandles/SafeResourceHandle.cs
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;
}
}
12 changes: 10 additions & 2 deletions Penumbra/Interop/Services/CharacterUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public CharacterUtilityData* Address
public event Action LoadingFinished;
public nint DefaultTransparentResource { get; private set; }
public nint DefaultDecalResource { get; private set; }
public nint DefaultSkinShpkResource { get; private set; }

/// <summary>
/// The relevant indices depend on which meta manipulations we allow for.
Expand Down Expand Up @@ -102,6 +103,12 @@ private void LoadDefaultResources(object _)
anyMissing |= DefaultDecalResource == nint.Zero;
}

if (DefaultSkinShpkResource == nint.Zero)
{
DefaultSkinShpkResource = (nint)Address->SkinShpkResource;
anyMissing |= DefaultSkinShpkResource == nint.Zero;
}

if (anyMissing)
return;

Expand Down Expand Up @@ -140,15 +147,16 @@ public MetaList.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx)

/// <summary> Return all relevant resources to the default resource. </summary>
public void ResetAll()
{
{
if (!Ready)
return;
return;

foreach (var list in _lists)
list.Dispose();

Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource;
Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource;
Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource;
}

public void Dispose()
Expand Down
170 changes: 170 additions & 0 deletions Penumbra/Interop/Services/SkinFixer.cs
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;
}
}
}
}
8 changes: 6 additions & 2 deletions Penumbra/Interop/Structs/CharacterUtilityData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ public unsafe struct CharacterUtilityData
{
public const int IndexTransparentTex = 72;
public const int IndexDecalTex = 73;
public const int IndexSkinShpk = 76;

public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >()
.Zip( Enum.GetValues< MetaIndex >() )
.Where( n => n.First.StartsWith( "Eqdp" ) )
.Select( n => n.Second ).ToArray();

public const int TotalNumResources = 87;

/// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary>

/// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary>
public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory )
=> +( int )raceCode switch
{
Expand Down Expand Up @@ -95,5 +96,8 @@ public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory )
[FieldOffset( 8 + IndexDecalTex * 8 )]
public TextureResourceHandle* DecalTexResource;

[FieldOffset( 8 + IndexSkinShpk * 8 )]
public ResourceHandle* SkinShpkResource;

// not included resources have no known use case.
}
Loading

0 comments on commit 5ba993c

Please sign in to comment.