Small changes.

This commit is contained in:
Ottermandias 2024-03-28 17:37:17 +01:00
parent 3066bf84d5
commit 1ba5011bfa
9 changed files with 120 additions and 95 deletions

View file

@ -8,7 +8,6 @@ using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Subclasses;
using Penumbra.Interop.SafeHandles;
namespace Penumbra.Collections.Cache;
@ -19,16 +18,16 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
/// The Cache contains all required temporary data to use a collection.
/// It will only be setup if a collection gets activated in any way.
/// </summary>
public class CollectionCache : IDisposable
public sealed class CollectionCache : IDisposable
{
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly ConcurrentDictionary<Utf8GamePath, SafeResourceHandle> LoadedResources = new();
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
public int Calculating = -1;
@ -39,7 +38,7 @@ public class CollectionCache : IDisposable
=> ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod)
=> ConflictDict.TryGetValue(mod, out SingleArray<ModConflicts> c) ? c : new SingleArray<ModConflicts>();
=> ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
private int _changedItemsSaveCounter = -1;
@ -56,16 +55,21 @@ public class CollectionCache : IDisposable
// The cache reacts through events on its collection changing.
public CollectionCache(CollectionCacheManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
CustomResources = new CustomResourceCache(manager.ResourceLoader);
}
public void Dispose()
=> Meta.Dispose();
{
Meta.Dispose();
CustomResources.Dispose();
GC.SuppressFinalize(this);
}
~CollectionCache()
=> Meta.Dispose();
=> Dispose();
// Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
@ -74,7 +78,7 @@ public class CollectionCache : IDisposable
return null;
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.Path.IsRooted && !candidate.Path.Exists)
|| candidate.Path is { IsRooted: true, Exists: false })
return null;
return candidate.Path;
@ -102,7 +106,7 @@ public class CollectionCache : IDisposable
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
{
if (fullPaths.Count == 0)
return Array.Empty<HashSet<Utf8GamePath>>();
return [];
var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
var dict = new Dictionary<FullPath, int>(fullPaths.Count);
@ -110,8 +114,8 @@ public class CollectionCache : IDisposable
{
dict[new FullPath(path)] = idx;
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
? new HashSet<Utf8GamePath> { utf8 }
: new HashSet<Utf8GamePath>();
? [utf8]
: [];
}
foreach (var (game, full) in ResolvedFiles)
@ -138,13 +142,6 @@ public class CollectionCache : IDisposable
public void RemoveMod(IMod mod, bool addMetaChanges)
=> _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges));
/// <summary> Invalidates caches subsequently to a resolved file being modified. </summary>
private void InvalidateResolvedFile(Utf8GamePath path)
{
if (LoadedResources.Remove(path, out var handle))
handle.Dispose();
}
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
internal void ForceFileSync(Utf8GamePath path, FullPath fullPath)
{
@ -157,20 +154,20 @@ public class CollectionCache : IDisposable
if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path,
Mod.ForcedFiles);
}
else
{
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null);
}
}
else if (fullPath.FullName.Length > 0)
{
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles);
}
}
@ -193,7 +190,7 @@ public class CollectionCache : IDisposable
{
if (ResolvedFiles.Remove(path, out var mp))
{
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
if (mp.Mod != mod)
Penumbra.Log.Warning(
$"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}.");
@ -308,7 +305,7 @@ public class CollectionCache : IDisposable
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
{
ModData.AddPath(mod, path);
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod);
return;
}
@ -323,14 +320,14 @@ public class CollectionCache : IDisposable
ModData.RemovePath(modPath.Mod, path);
ResolvedFiles[path] = new ModPath(mod, file);
ModData.AddPath(mod, path);
InvalidateResolvedFile(path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod);
}
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
$"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}");
}
}

View file

@ -4,6 +4,7 @@ using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
@ -21,8 +22,8 @@ public class CollectionCacheManager : IDisposable
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
internal readonly ResolvedFileChanged ResolvedFileChanged;
internal readonly MetaFileManager MetaFileManager;
internal readonly MetaFileManager MetaFileManager;
internal readonly ResourceLoader ResourceLoader;
private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new();
@ -35,7 +36,7 @@ public class CollectionCacheManager : IDisposable
=> _storage.Where(c => c.HasCache);
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage,
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage)
MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader)
{
_framework = framework;
_communicator = communicator;
@ -44,6 +45,7 @@ public class CollectionCacheManager : IDisposable
MetaFileManager = metaFileManager;
_active = active;
_storage = storage;
ResourceLoader = resourceLoader;
ResolvedFileChanged = _communicator.ResolvedFileChanged;
if (!_active.Individuals.IsLoaded)

View file

@ -0,0 +1,49 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.SafeHandles;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache;
/// <summary> A cache for resources owned by a collection. </summary>
public sealed class CustomResourceCache(ResourceLoader loader)
: ConcurrentDictionary<Utf8GamePath, SafeResourceHandle>, IDisposable
{
/// <summary> Invalidate an existing resource by clearing it from the cache and disposing it. </summary>
public void Invalidate(Utf8GamePath path)
{
if (TryRemove(path, out var handle))
handle.Dispose();
}
public void Dispose()
{
foreach (var handle in Values)
handle.Dispose();
Clear();
}
/// <summary> Get the requested resource either from the cached resource, or load a new one if it does not exist. </summary>
public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data)
{
if (TryGetClonedValue(path, out var handle))
return handle;
handle = loader.LoadResolvedSafeResource(category, type, path.Path, data);
var clone = handle.Clone();
if (!TryAdd(path, clone))
clone.Dispose();
return handle;
}
/// <summary> Get a cloned cached resource if it exists. </summary>
private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle)
{
if (!TryGetValue(path, out handle))
return false;
handle = handle.Clone();
return true;
}
}

View file

@ -49,24 +49,6 @@ public unsafe class ResourceLoader : IDisposable
return ret;
}
public SafeResourceHandle LoadCacheableSafeResource(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData resolveData)
{
var cache = resolveData.ModCollection._cache;
if (cache == null)
return LoadResolvedSafeResource(category, type, path.Path, resolveData);
if (cache.LoadedResources.TryGetValue(path, out var cached))
return cached.Clone();
var ret = LoadResolvedSafeResource(category, type, path.Path, resolveData);
cached = ret.Clone();
if (!cache.LoadedResources.TryAdd(path, cached))
cached.Dispose();
return ret;
}
/// <summary> The function to use to resolve a given path. </summary>
public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;

View file

@ -156,7 +156,7 @@ public class ResourceTree
var genericContext = globalContext.CreateContext(&human->CharacterBase);
var cache = globalContext.Collection._cache;
if (cache != null && cache.LoadedResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle))
if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle))
{
var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle);
if (pbdNode != null)

View file

@ -1,10 +1,9 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.SafeHandles;
@ -12,14 +11,11 @@ using Penumbra.String.Classes;
namespace Penumbra.Interop.Services;
public sealed unsafe class PreBoneDeformerReplacer : IDisposable
public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService
{
public static readonly Utf8GamePath PreBoneDeformerPath =
Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty;
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
// Approximate name guesses.
private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex);
private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex);
@ -32,15 +28,16 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable
private readonly ResourceLoader _resourceLoader;
private readonly IFramework _framework;
public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, IGameInteropProvider interop, IFramework framework)
public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader,
IGameInteropProvider interop, IFramework framework, CharacterBaseVTables vTables)
{
interop.InitializeFromAttributes(this);
_utility = utility;
_collectionResolver = collectionResolver;
_resourceLoader = resourceLoader;
_framework = framework;
_humanSetupScalingHook = interop.HookFromAddress<CharacterBaseSetupScalingDelegate>(_humanVTable[57], SetupScaling);
_humanCreateDeformerHook = interop.HookFromAddress<CharacterBaseCreateDeformerDelegate>(_humanVTable[91], CreateDeformer);
_utility = utility;
_collectionResolver = collectionResolver;
_resourceLoader = resourceLoader;
_framework = framework;
_humanSetupScalingHook = interop.HookFromAddress<CharacterBaseSetupScalingDelegate>(vTables.HumanVTable[57], SetupScaling);
_humanCreateDeformerHook = interop.HookFromAddress<CharacterBaseCreateDeformerDelegate>(vTables.HumanVTable[91], CreateDeformer);
_humanSetupScalingHook.Enable();
_humanCreateDeformerHook.Enable();
}
@ -54,13 +51,17 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable
private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject)
{
var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true);
return _resourceLoader.LoadCacheableSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData);
if (resolveData.ModCollection._cache is not { } cache)
return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData);
return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData);
}
private void SetupScaling(CharacterBase* drawObject, uint slotIndex)
{
if (!_framework.IsInFrameworkUpdateThread)
Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
Penumbra.Log.Warning(
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
try
@ -78,7 +79,8 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable
private void* CreateDeformer(CharacterBase* drawObject, uint slotIndex)
{
if (!_framework.IsInFrameworkUpdateThread)
Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
Penumbra.Log.Warning(
$"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
try

View file

@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Communication;
using Penumbra.GameData;
using Penumbra.Interop.Hooks.Resources;
@ -13,7 +14,7 @@ using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRen
namespace Penumbra.Interop.Services;
public sealed unsafe class ShaderReplacementFixer : IDisposable
public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService
{
public static ReadOnlySpan<byte> SkinShpkName
=> "skin.shpk"u8;
@ -21,11 +22,10 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
public static ReadOnlySpan<byte> CharacterGlassShpkName
=> "characterglass.shpk"u8;
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param);
private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex);
private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags,
CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex);
private readonly Hook<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook;
@ -59,14 +59,15 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
=> _moddedCharacterGlassShpkCount;
public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer,
CommunicatorService communicator, IGameInteropProvider interop)
CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables)
{
interop.InitializeFromAttributes(this);
_resourceHandleDestructor = resourceHandleDestructor;
_utility = utility;
_modelRenderer = modelRenderer;
_communicator = communicator;
_humanOnRenderMaterialHook = interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
_resourceHandleDestructor = resourceHandleDestructor;
_utility = utility;
_modelRenderer = modelRenderer;
_communicator = communicator;
_humanOnRenderMaterialHook =
interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(vTables.HumanVTable[62], OnRenderHumanMaterial);
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer);
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
_humanOnRenderMaterialHook.Enable();
@ -82,7 +83,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
_moddedCharacterGlassShpkMaterials.Clear();
_moddedSkinShpkMaterials.Clear();
_moddedCharacterGlassShpkCount = 0;
_moddedSkinShpkCount = 0;
_moddedSkinShpkCount = 0;
}
public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas()
@ -106,16 +107,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
var shpkName = mtrl->ShpkNameSpan;
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);
}
}
private void OnResourceHandleDestructor(Structs.ResourceHandle* handle)
@ -159,9 +156,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
}
}
private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex)
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 || _moddedCharacterGlassShpkCount == 0)
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);

View file

@ -77,8 +77,6 @@ public class Penumbra : IDalamudPlugin
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches();
_services.GetService<PathResolver>();
_services.GetService<PreBoneDeformerReplacer>();
_services.GetService<ShaderReplacementFixer>();
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.

View file

@ -131,9 +131,7 @@ public static class StaticServiceManager
=> services.AddSingleton<ResourceLoader>()
.AddSingleton<ResourceWatcher>()
.AddSingleton<ResourceTreeFactory>()
.AddSingleton<MetaFileManager>()
.AddSingleton<PreBoneDeformerReplacer>()
.AddSingleton<ShaderReplacementFixer>();
.AddSingleton<MetaFileManager>();
private static ServiceManager AddResolvers(this ServiceManager services)
=> services.AddSingleton<CollectionResolver>()