Merge branch 'async-stuff'

# Conflicts:
#	Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs
This commit is contained in:
Ottermandias 2025-01-25 13:55:54 +01:00
commit e508e6158f
11 changed files with 307 additions and 69 deletions

@ -1 +1 @@
Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262
Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19

View file

@ -100,6 +100,7 @@ public class HookOverrides
public bool DecRef;
public bool GetResourceSync;
public bool GetResourceAsync;
public bool UpdateResourceState;
public bool CheckFileState;
public bool TexResourceHandleOnLoad;
public bool LoadMdlFileExtern;

View file

@ -2,6 +2,7 @@ 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 +14,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<nint, Utf8GamePath> _ongoingLoads = [];
private ResolveData _resolvedData = ResolveData.Invalid;
public event Action<Utf8GamePath, FullPath?, ResolveData>? PapRequested;
public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner)
public IReadOnlyDictionary<nint, Utf8GamePath> 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 +121,32 @@ public unsafe class ResourceLoader : IDisposable, IService
/// </summary>
public event FileLoadedDelegate? FileLoaded;
public delegate void ResourceCompleteDelegate(ResourceHandle* resource, CiByteString path, Utf8GamePath originalPath,
ReadOnlySpan<byte> additionalData, bool isAsync);
/// <summary>
/// Event fired just before a resource finishes loading.
/// <see cref="ResourceHandle.LoadState"/> 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.
/// </summary>
public event ResourceCompleteDelegate? BeforeResourceComplete;
/// <summary>
/// Event fired when a resource has finished loading.
/// <see cref="ResourceHandle.LoadState"/> 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.
/// </summary>
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 +167,8 @@ public unsafe class ResourceLoader : IDisposable, IService
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, original, parameters);
TrackResourceLoad(returnValue, original);
ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data);
return;
}
@ -145,10 +178,57 @@ public unsafe class ResourceLoader : IDisposable, IService
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, original, parameters);
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 is { Item1: 2, 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.Excessive($"[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.Excessive($"[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)
@ -176,7 +256,6 @@ public unsafe class ResourceLoader : IDisposable, IService
gamePath.Path.IsAscii);
fileDescriptor->ResourceHandle->FileNameData = path.Path;
fileDescriptor->ResourceHandle->FileNameLength = path.Length;
ForceSync(fileDescriptor, ref isSync);
returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data);
// Return original resource handle path so that they can be loaded separately.
fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path;
@ -215,16 +294,6 @@ public unsafe class ResourceLoader : IDisposable, IService
}
}
/// <summary> Special handling for materials and IMCs. </summary>
private static void ForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync)
{
// Force isSync = true for Materials. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl or ResourceType.Imc;
}
/// <summary>
/// A resource with ref count 0 that gets incremented goes through GetResourceAsync again.
/// This means, that if the path determined from that is different than the resources path,
@ -265,6 +334,11 @@ public unsafe class ResourceLoader : IDisposable, IService
returnValue = 1;
}
private void ResourceDestructorHandler(ResourceHandle* handle)
{
_ongoingLoads.TryRemove((nint)handle, out _);
}
/// <summary> Compute the CRC32 hash for a given path together with potential resource parameters. </summary>
private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams)
{

View file

@ -19,6 +19,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
private readonly PerformanceTracker _performance;
private readonly ResourceManagerService _resourceManager;
private readonly ThreadLocal<Utf8GamePath> _currentGetResourcePath = new(() => Utf8GamePath.Empty);
public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop)
{
_performance = performance;
@ -34,6 +36,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
_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 unsafe class ResourceService : IDisposable, IRequiredService
{
_getResourceSyncHook.Dispose();
_getResourceAsyncHook.Dispose();
_updateResourceStateHook.Dispose();
_incRefHook.Dispose();
_decRefHook.Dispose();
_currentGetResourcePath.Dispose();
}
#region GetResource
@ -112,28 +118,82 @@ public unsafe class ResourceService : IDisposable, IRequiredService
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, original, pGetResParams, isUnk, unk8, unk9);
}
/// <summary> Call the original GetResource function. </summary>
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path,
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original,
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);
{
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
/// <summary> Invoked before a resource state is updated. </summary>
/// <param name="handle">The resource handle.</param>
/// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
public delegate void ResourceStateUpdatingDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal);
/// <summary> Invoked after a resource state is updated. </summary>
/// <param name="handle">The resource handle.</param>
/// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
/// <param name="previousState">The previous state of the resource.</param>
/// <param name="returnValue">The return value to use.</param>
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
/// <summary>
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceStateUpdatingDelegate? ResourceStateUpdating;
/// <summary>
/// <inheritdoc cref="ResourceStateUpdatedDelegate"/> <para/>
/// Subscribers should be exception-safe.
/// </summary>
public event ResourceStateUpdatedDelegate? ResourceStateUpdated;
private delegate uint UpdateResourceStatePrototype(ResourceHandle* handle, byte offFileThread);
[Signature(Sigs.UpdateResourceState, DetourName = nameof(UpdateResourceStateDetour))]
private readonly Hook<UpdateResourceStatePrototype> _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
/// <summary> Invoked before a resource handle reference count is incremented. </summary>

View file

@ -14,9 +14,12 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr<ResourceHa
/// <seealso cref="PathResolving.SubfileHelper"/>
SubfileHelper,
/// <seealso cref="ShaderReplacementFixer"/>
/// <seealso cref="PostProcessing.ShaderReplacementFixer"/>
ShaderReplacementFixer,
/// <seealso cref="ResourceLoading.ResourceLoader"/>
ResourceLoader,
/// <seealso cref="ResourceWatcher.OnResourceDestroyed"/>
ResourceWatcher,
}

View file

@ -4,6 +4,7 @@ using Penumbra.Api.Enums;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Processing;
@ -20,20 +21,20 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable
public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services)
{
_resourceLoader = resourceLoader;
_processors = services.GetServicesImplementing<IFilePostProcessor>().ToFrozenDictionary(s => s.Type, s => s);
_resourceLoader.FileLoaded += OnFileLoaded;
_resourceLoader = resourceLoader;
_processors = services.GetServicesImplementing<IFilePostProcessor>().ToFrozenDictionary(s => s.Type, s => s);
_resourceLoader.BeforeResourceComplete += OnBeforeResourceComplete;
}
public void Dispose()
{
_resourceLoader.FileLoaded -= OnFileLoaded;
_resourceLoader.BeforeResourceComplete -= OnBeforeResourceComplete;
}
private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom,
ReadOnlySpan<byte> additionalData)
private void OnBeforeResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original,
ReadOnlySpan<byte> additionalData, bool isAsync)
{
if (_processors.TryGetValue(resource->FileType, out var processor))
processor.PostProcess(resource, path, additionalData);
processor.PostProcess(resource, original.Path, additionalData);
}
}

View file

@ -24,10 +24,20 @@ public unsafe struct TextureResourceHandle
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 unsafe struct ResourceHandle
[FieldOffset(0x58)]
public int FileNameLength;
[FieldOffset(0xA8)]
public byte UnkState;
[FieldOffset(0xA9)]
public LoadState LoadState;

View file

@ -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,37 @@ internal unsafe struct Record
LoadState = handle->LoadState,
Crc64 = 0,
};
public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan<byte> additionalData)
=> new()
{
Time = DateTime.UtcNow,
Path = CombinedPath(path, additionalData),
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,
};
private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan<byte> additionalData)
{
if (additionalData.Length is 0)
return path.IsOwned ? path : path.Clone();
fixed (byte* ptr = additionalData)
{
// If a path has additional data and is split, it is always in the form of |{additionalData}|{path},
// so we can just read from the start of additional data - 1 and sum their length +2 for the pipes.
return new CiByteString(new ReadOnlySpan<byte>(ptr - 1, additionalData.Length + 2 + path.Length)).Clone();
}
}
}

View file

@ -47,9 +47,10 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService
_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 sealed class ResourceWatcher : IDisposable, ITab, IUiService
_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 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService
_newRecords.Enqueue(record);
}
private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan<byte> additionalData, 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, additionalData);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record);
}
private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan<byte> _)
{
if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match))

View file

@ -124,11 +124,12 @@ internal sealed class ResourceWatcherTable : Table<Record>
{
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 @@ internal sealed class ResourceWatcherTable : Table<Record>
{
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 @@ internal sealed class ResourceWatcherTable : Table<Record>
{
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))
{

View file

@ -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 class DebugTab : Window, ITab, IUiService
_actors = actors;
_stains = stains;
_resourceManager = resourceManager;
_resourceLoader = resourceLoader;
_collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_pathState = pathState;
@ -191,6 +193,7 @@ public class DebugTab : Window, ITab, IUiService
DrawShaderReplacementFixer();
DrawData();
DrawCrcCache();
DrawResourceLoader();
DrawResourceProblems();
_renderTargetDrawer.Draw();
_hookOverrides.Draw();
@ -1099,6 +1102,35 @@ public class DebugTab : Window, ITab, IUiService
}
}
private unsafe void DrawResourceLoader()
{
if (!ImUtf8.CollapsingHeader("Resource Loader"u8))
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)
{
ImUtf8.DrawTableColumn($"0x{handle:X}");
ImUtf8.DrawTableColumn(((ResourceHandle*)handle)->CsHandle.FileName);
ImUtf8.DrawTableColumn(original.Path.Span);
}
}
/// <summary> Draw resources with unusual reference count. </summary>
private unsafe void DrawResourceProblems()
{