From a3ddce0ef5aed4a0282039e79d46417b108c5716 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 24 Jan 2025 02:50:02 +0100 Subject: [PATCH] Add mechanism to handle completion of async res loads --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../Hooks/ResourceLoading/ResourceLoader.cs | 120 +++++++++++++++--- .../Hooks/ResourceLoading/ResourceService.cs | 78 ++++++++++-- .../Resources/ResourceHandleDestructor.cs | 5 +- .../Processing/FilePostProcessService.cs | 18 ++- Penumbra/Interop/Structs/ResourceHandle.cs | 15 ++- Penumbra/UI/ResourceWatcher/Record.cs | 29 ++++- .../UI/ResourceWatcher/ResourceWatcher.cs | 31 ++++- .../ResourceWatcher/ResourceWatcherTable.cs | 25 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 37 +++++- 11 files changed, 303 insertions(+), 58 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ebeea67c..4a987167 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262 +Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 5a856764..bcff25d2 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -100,6 +100,7 @@ public struct ResourceLoadingHooks public bool DecRef; public bool GetResourceSync; public bool GetResourceAsync; + public bool UpdateResourceState; public bool CheckFileState; public bool TexResourceHandleOnLoad; public bool LoadMdlFileExtern; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index ad9c41e6..f9b8ff60 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,7 +1,9 @@ +using System.IO; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -13,27 +15,38 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceLoader : IDisposable, IService { - private readonly ResourceService _resources; - private readonly FileReadService _fileReadService; - private readonly RsfService _rsfService; - private readonly PapHandler _papHandler; - private readonly Configuration _config; + private readonly ResourceService _resources; + private readonly FileReadService _fileReadService; + private readonly RsfService _rsfService; + private readonly PapHandler _papHandler; + private readonly Configuration _config; + private readonly ResourceHandleDestructor _destructor; + + private readonly ConcurrentDictionary _ongoingLoads = []; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner) + public IReadOnlyDictionary OngoingLoads + => _ongoingLoads; + + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner, + ResourceHandleDestructor destructor) { _resources = resources; _fileReadService = fileReadService; - _rsfService = rsfService; + _rsfService = rsfService; _config = config; + _destructor = destructor; ResetResolvePath(); - _resources.ResourceRequested += ResourceHandler; - _resources.ResourceHandleIncRef += IncRefProtection; - _resources.ResourceHandleDecRef += DecRefProtection; - _fileReadService.ReadSqPack += ReadSqPackDetour; + _resources.ResourceRequested += ResourceHandler; + _resources.ResourceStateUpdating += ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated += ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef += IncRefProtection; + _resources.ResourceHandleDecRef += DecRefProtection; + _fileReadService.ReadSqPack += ReadSqPackDetour; + _destructor.Subscribe(ResourceDestructorHandler, ResourceHandleDestructor.Priority.ResourceLoader); _papHandler = new PapHandler(sigScanner, PapResourceHandler); _papHandler.Enable(); @@ -109,12 +122,32 @@ public delegate void FileLoadedDelegate(ResourceHandle* resource, CiByteString p /// public event FileLoadedDelegate? FileLoaded; + public delegate void ResourceCompleteDelegate(ResourceHandle* resource, CiByteString path, Utf8GamePath originalPath, + ReadOnlySpan additionalData, bool isAsync); + + /// + /// Event fired just before a resource finishes loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? BeforeResourceComplete; + + /// + /// Event fired when a resource has finished loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? ResourceComplete; + public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; - _resources.ResourceHandleIncRef -= IncRefProtection; - _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceStateUpdating -= ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated -= ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef -= IncRefProtection; + _resources.ResourceHandleDecRef -= DecRefProtection; + _fileReadService.ReadSqPack -= ReadSqPackDetour; + _destructor.Unsubscribe(ResourceDestructorHandler); _papHandler.Dispose(); } @@ -135,7 +168,8 @@ private void ResourceHandler(ref ResourceCategory category, ref ResourceType typ if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; } @@ -145,10 +179,57 @@ private void ResourceHandler(ref ResourceCategory category, ref ResourceType typ hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } + private void TrackResourceLoad(ResourceHandle* handle, Utf8GamePath original) + { + if (handle->UnkState == 2 && handle->LoadState >= LoadState.Success) + return; + + _ongoingLoads.TryAdd((nint)handle, original.Clone()); + } + + private void ResourceStateUpdatedHandler(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte, LoadState) previousState, ref uint returnValue) + { + if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState.Item1 == 2 && previousState.Item2 >= LoadState.Success) + return; + + if (!_ongoingLoads.TryRemove((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + if (!syncOriginal.IsEmpty && !asyncOriginal.IsEmpty && !syncOriginal.Equals(asyncOriginal)) + Penumbra.Log.Warning($"[ResourceLoader] Resource original paths inconsistency: 0x{(nint)handle:X}, of path {path}, sync original {syncOriginal}, async original {asyncOriginal}."); + var original = !asyncOriginal.IsEmpty ? asyncOriginal : syncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + ResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + ResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + + private void ResourceStateUpdatingHandler(ResourceHandle* handle, Utf8GamePath syncOriginal) + { + if (handle->UnkState != 1 || handle->LoadState != LoadState.Success) + return; + + if (!_ongoingLoads.TryGetValue((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + var original = asyncOriginal.IsEmpty ? syncOriginal : asyncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + BeforeResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + BeforeResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) { if (fileDescriptor->ResourceHandle == null) @@ -265,6 +346,11 @@ private static void DecRefProtection(ResourceHandle* handle, ref byte? returnVal returnValue = 1; } + private void ResourceDestructorHandler(ResourceHandle* handle) + { + _ongoingLoads.TryRemove((nint)handle, out _); + } + /// Compute the CRC32 hash for a given path together with potential resource parameters. private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams) { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 126505d1..238ed70f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -19,6 +19,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; + private readonly ThreadLocal _currentGetResourcePath = new(() => Utf8GamePath.Empty); + public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _performance = performance; @@ -34,6 +36,8 @@ public ResourceService(PerformanceTracker performance, ResourceManagerService re _getResourceSyncHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); + if (!HookOverrides.Instance.ResourceLoading.UpdateResourceState) + _updateResourceStateHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.DecRef) @@ -54,8 +58,10 @@ public void Dispose() { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); + _updateResourceStateHook.Dispose(); _incRefHook.Dispose(); _decRefHook.Dispose(); + _currentGetResourcePath.Dispose(); } #region GetResource @@ -112,28 +118,84 @@ public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref R unk9); } + var original = gamePath; ResourceHandle* returnValue = null; - ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, gamePath, pGetResParams, ref isSync, + ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9, original); } /// Call the original GetResource function. public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) - => sync - ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk8, unk9) - : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk, unk8, unk9); + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0, Utf8GamePath original = default) + { + if (original.Path is null) // i. e. if original is default + Utf8GamePath.FromByteString(path, out original); + var previous = _currentGetResourcePath.Value; + try + { + _currentGetResourcePath.Value = original; + return sync + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk8, unk9) + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk, unk8, unk9); + } finally + { + _currentGetResourcePath.Value = previous; + } + } #endregion private delegate nint ResourceHandlePrototype(ResourceHandle* handle); + #region UpdateResourceState + + /// Invoked before a resource state is updated. + /// The resource handle. + /// The original game path of the resource, if loaded synchronously. + public delegate void ResourceStateUpdatingDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal); + + /// Invoked after a resource state is updated. + /// The resource handle. + /// The original game path of the resource, if loaded synchronously. + /// The previous state of the resource. + /// The return value to use. + public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatingDelegate? ResourceStateUpdating; + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatedDelegate? ResourceStateUpdated; + + private delegate uint UpdateResourceStatePrototype(ResourceHandle* handle, byte offFileThread); + + [Signature(Sigs.UpdateResourceState, DetourName = nameof(UpdateResourceStateDetour))] + private readonly Hook _updateResourceStateHook = null!; + + private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) + { + var previousState = (handle->UnkState, handle->LoadState); + var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value! : Utf8GamePath.Empty; + ResourceStateUpdating?.Invoke(handle, syncOriginal); + var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); + ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); + return ret; + } + + #endregion + #region IncRef /// Invoked before a resource handle reference count is incremented. diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index bdb11752..0e04029b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -14,9 +14,12 @@ public enum Priority /// SubfileHelper, - /// + /// ShaderReplacementFixer, + /// + ResourceLoader, + /// ResourceWatcher, } diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index ecf78c69..a27f6d45 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -4,6 +4,7 @@ using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Processing; @@ -20,20 +21,23 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) { - _resourceLoader = resourceLoader; - _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.FileLoaded += OnFileLoaded; + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.BeforeResourceComplete += OnResourceComplete; } public void Dispose() { - _resourceLoader.FileLoaded -= OnFileLoaded; + _resourceLoader.BeforeResourceComplete -= OnResourceComplete; } - private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) + private void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + ReadOnlySpan additionalData, bool isAsync) { + if (resource->LoadState != LoadState.Success) + return; + if (_processors.TryGetValue(resource->FileType, out var processor)) - processor.PostProcess(resource, path, additionalData); + processor.PostProcess(resource, original.Path, additionalData); } } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 65550563..1558c035 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -24,10 +24,20 @@ public bool ChangeLod public enum LoadState : byte { + Constructing = 0x00, + Constructed = 0x01, + Async2 = 0x02, + AsyncRequested = 0x03, + Async4 = 0x04, + AsyncLoading = 0x05, + Async6 = 0x06, Success = 0x07, - Async = 0x03, + Unknown8 = 0x08, Failure = 0x09, FailedSubResource = 0x0A, + FailureB = 0x0B, + FailureC = 0x0C, + FailureD = 0x0D, None = 0xFF, } @@ -74,6 +84,9 @@ public readonly bool GamePath(out Utf8GamePath path) [FieldOffset(0x58)] public int FileNameLength; + [FieldOffset(0xA8)] + public byte UnkState; + [FieldOffset(0xA9)] public LoadState LoadState; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 7338e5a9..13a71656 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -10,10 +10,11 @@ namespace Penumbra.UI.ResourceWatcher; [Flags] public enum RecordType : byte { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, + ResourceComplete = 0x10, } internal unsafe struct Record @@ -141,4 +142,24 @@ public static Record CreateFileLoad(CiByteString path, ResourceHandle* handle, b LoadState = handle->LoadState, Crc64 = 0, }; + + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceComplete, + Synchronously = false, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, + LoadState = handle->LoadState, + Crc64 = 0, + }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 0f72efff..53d7e79d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -47,9 +47,10 @@ public unsafe ResourceWatcher(ActorManager actors, Configuration config, Resourc _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; - _loader.PapRequested += OnPapRequested; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.ResourceComplete += OnResourceComplete; + _loader.FileLoaded += OnFileLoaded; + _loader.PapRequested += OnPapRequested; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } @@ -73,9 +74,10 @@ public unsafe void Dispose() _records.TrimExcess(); _resources.ResourceRequested -= OnResourceRequested; _destructor.Unsubscribe(OnResourceDestroyed); - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; - _loader.PapRequested -= OnPapRequested; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.ResourceComplete -= OnResourceComplete; + _loader.FileLoaded -= OnFileLoaded; + _loader.PapRequested -= OnPapRequested; } private void Clear() @@ -255,6 +257,23 @@ private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, _newRecords.Enqueue(record); } + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan _, bool isAsync) + { + if (!isAsync) + return; + + if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) + Penumbra.Log.Information( + $"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}."); + + if (!_ephemeral.EnableResourceWatcher) + return; + + var record = Record.CreateResourceComplete(path, resource, original); + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 7ac3cb99..a58d74d1 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -124,11 +124,12 @@ public override void DrawColumn(Record item, int idx) { ImGui.TextUnformatted(item.RecordType switch { - RecordType.Request => "REQ", - RecordType.ResourceLoad => "LOAD", - RecordType.FileLoad => "FILE", - RecordType.Destruction => "DEST", - _ => string.Empty, + RecordType.Request => "REQ", + RecordType.ResourceLoad => "LOAD", + RecordType.FileLoad => "FILE", + RecordType.Destruction => "DEST", + RecordType.ResourceComplete => "DONE", + _ => string.Empty, }); } } @@ -317,10 +318,10 @@ public override bool FilterFunc(Record item) { LoadState.None => FilterValue.HasFlag(LoadStateFlag.None), LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success), - LoadState.Async => FilterValue.HasFlag(LoadStateFlag.Async), - LoadState.Failure => FilterValue.HasFlag(LoadStateFlag.Failed), LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub), - _ => FilterValue.HasFlag(LoadStateFlag.Unknown), + <= LoadState.Constructed => FilterValue.HasFlag(LoadStateFlag.Unknown), + < LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Async), + > LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Failed), }; public override void DrawColumn(Record item, int _) @@ -332,12 +333,12 @@ public override void DrawColumn(Record item, int _) { LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), $"Successfully loaded ({(byte)item.LoadState})."), - LoadState.Async => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), - LoadState.Failure => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), - $"Failed to load ({(byte)item.LoadState})."), LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), $"Dependencies failed to load ({(byte)item.LoadState})."), - _ => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Unknown state ({(byte)item.LoadState})."), + <= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Not yet loaded ({(byte)item.LoadState})."), + < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), + > LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), + $"Failed to load ({(byte)item.LoadState})."), }; using (var font = ImRaii.PushFont(UiBuilder.IconFont)) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ad4824c3..5dc203c2 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -80,6 +80,7 @@ public class DebugTab : Window, ITab, IUiService private readonly StainService _stains; private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly ResourceManagerService _resourceManager; + private readonly ResourceLoader _resourceLoader; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly PathState _pathState; @@ -109,7 +110,7 @@ public class DebugTab : Window, ITab, IUiService public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, - ResourceManagerService resourceManager, CollectionResolver collectionResolver, + ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, @@ -133,6 +134,7 @@ public DebugTab(PerformanceTracker performance, Configuration config, Collection _actors = actors; _stains = stains; _resourceManager = resourceManager; + _resourceLoader = resourceLoader; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _pathState = pathState; @@ -191,6 +193,7 @@ public void DrawContent() DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); + DrawResourceLoader(); DrawResourceProblems(); _renderTargetDrawer.Draw(); _hookOverrides.Draw(); @@ -1099,6 +1102,38 @@ private unsafe void DrawCrcCache() } } + private unsafe void DrawResourceLoader() + { + if (!ImGui.CollapsingHeader("Resource Loader")) + return; + + var ongoingLoads = _resourceLoader.OngoingLoads; + var ongoingLoadCount = ongoingLoads.Count; + ImUtf8.Text($"Ongoing Loads: {ongoingLoadCount}"); + + if (ongoingLoadCount == 0) + return; + + using var table = ImUtf8.Table("ongoingLoadTable"u8, 3); + if (!table) + return; + + ImUtf8.TableSetupColumn("Resource Handle"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Actual Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImUtf8.TableSetupColumn("Original Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (handle, original) in ongoingLoads) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"0x{handle:X}"); + ImGui.TableNextColumn(); + ImUtf8.Text(((ResourceHandle*)handle)->CsHandle.FileName); + ImGui.TableNextColumn(); + ImUtf8.Text(original.Path.Span); + } + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() {