mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 07:17:53 +01:00
Merge branch 'master' into feature/material-editor-2099
This commit is contained in:
commit
8695e89792
15 changed files with 923 additions and 255 deletions
|
|
@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable
|
|||
_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;
|
||||
}
|
||||
|
||||
|
|
|
|||
34
Penumbra/Interop/SafeHandles/SafeResourceHandle.cs
Normal file
34
Penumbra/Interop/SafeHandles/SafeResourceHandle.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable
|
|||
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.
|
||||
|
|
@ -102,6 +103,12 @@ public unsafe partial class CharacterUtility : IDisposable
|
|||
anyMissing |= DefaultDecalResource == nint.Zero;
|
||||
}
|
||||
|
||||
if (DefaultSkinShpkResource == nint.Zero)
|
||||
{
|
||||
DefaultSkinShpkResource = (nint)Address->SkinShpkResource;
|
||||
anyMissing |= DefaultSkinShpkResource == nint.Zero;
|
||||
}
|
||||
|
||||
if (anyMissing)
|
||||
return;
|
||||
|
||||
|
|
@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable
|
|||
|
||||
/// <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()
|
||||
|
|
|
|||
170
Penumbra/Interop/Services/SkinFixer.cs
Normal file
170
Penumbra/Interop/Services/SkinFixer.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ 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 >() )
|
||||
|
|
@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData
|
|||
.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
|
||||
{
|
||||
|
|
@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData
|
|||
[FieldOffset( 8 + IndexDecalTex * 8 )]
|
||||
public TextureResourceHandle* DecalTexResource;
|
||||
|
||||
[FieldOffset( 8 + IndexSkinShpk * 8 )]
|
||||
public ResourceHandle* SkinShpkResource;
|
||||
|
||||
// not included resources have no known use case.
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue