From 6a0f7746252cea35f3313cac01ad2564c9d94198 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 01:06:02 +0900 Subject: [PATCH] Add texture leak tracker --- .../Internal/DalamudConfiguration.cs | 3 + .../Windows/Data/Widgets/TexWidget.cs | 105 ++++- .../Internals/FontAtlasFactory.cs | 6 +- .../FileSystemSharedImmediateTexture.cs | 4 +- .../GamePathSharedImmediateTexture.cs | 4 +- .../ManifestResourceSharedImmediateTexture.cs | 4 +- .../Internal/TextureManager.BlameTracker.cs | 373 ++++++++++++++++++ .../TextureManager.FromExistingTexture.cs | 16 +- .../Textures/Internal/TextureManager.cs | 63 +-- .../Internal/TextureManagerPluginScoped.cs | 6 +- .../Textures/Internal/ViewportTextureWrap.cs | 22 +- Dalamud/Interface/UiBuilder.cs | 20 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Utility/TerraFxCom/ManagedIStream.cs | 32 +- 14 files changed, 597 insertions(+), 63 deletions(-) create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 85a9507c9..6114c0f62 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -443,6 +443,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// public double UiBuilderHitch { get; set; } = 100; + /// Gets or sets a value indicating whether to track texture allocation by plugins. + public bool UseTexturePluginTracking { get; set; } + /// /// Gets or sets the page of the plugin installer that is shown by default when opened. /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 092b3f74f..d14aeb5ab 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; @@ -108,6 +109,11 @@ internal class TexWidget : IDataWindowWidget if (ImGui.Button("GC")) GC.Collect(); + ImGui.PushID("blames"); + if (ImGui.CollapsingHeader($"All Loaded Textures: {this.textureManager.AllBlamesForDebug.Count:g}###header")) + this.DrawBlame(this.textureManager.AllBlamesForDebug); + ImGui.PopID(); + ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header")) @@ -292,6 +298,96 @@ internal class TexWidget : IDataWindowWidget this.fileDialogManager.Draw(); } + private unsafe void DrawBlame(IReadOnlyList allBlames) + { + var conf = Service.Get(); + var im = Service.Get(); + var blame = conf.UseTexturePluginTracking; + if (ImGui.Checkbox("Enable", ref blame)) + { + conf.UseTexturePluginTracking = blame; + conf.QueueSave(); + } + + if (!ImGui.BeginTable("##table", 5)) + return; + + const int numIcons = 1; + float iconWidths; + using (im.IconFontHandle?.Push()) + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00000x00000").X); + ImGui.TableSetupColumn( + "Format", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons)); + ImGui.TableSetupColumn( + "Plugins", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X); + ImGui.TableHeadersRow(); + + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + clipper.Begin(allBlames.Count); + + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + var wrap = allBlames[i]; + ImGui.TableNextRow(); + ImGui.PushID(i); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{wrap.Width}x{wrap.Height}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(wrap.Name); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + this.SaveTextureAsync( + $"{wrap.ImGuiHandle:X16}", + () => Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Image(wrap.ImGuiHandle, wrap.Size); + ImGui.EndTooltip(); + } + + ImGui.TableNextColumn(); + lock (wrap.OwnerPlugins) + { + foreach (var plugin in wrap.OwnerPlugins) + ImGui.TextUnformatted(plugin.Name); + } + + ImGui.PopID(); + } + } + + clipper.Destroy(); + ImGui.EndTable(); + + ImGuiHelpers.ScaledDummy(10); + } + private unsafe void DrawLoadedTextures(ICollection textures) { var im = Service.Get(); @@ -354,6 +450,7 @@ internal class TexWidget : IDataWindowWidget } var remain = texture.SelfReferenceExpiresInForDebug; + ImGui.PushID(row); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -400,6 +497,8 @@ internal class TexWidget : IDataWindowWidget ImGui.SetTooltip("Release self-reference immediately."); if (remain <= 0) ImGui.EndDisabled(); + + ImGui.PopID(); } if (!valid) @@ -609,7 +708,8 @@ internal class TexWidget : IDataWindowWidget new() { Api10 = this.textureManager.CreateFromImGuiViewportAsync( - this.viewportTextureArgs with { ViewportId = viewports[this.viewportIndexInt].ID }), + this.viewportTextureArgs with { ViewportId = viewports[this.viewportIndexInt].ID }, + null), }); } } @@ -758,6 +858,9 @@ internal class TexWidget : IDataWindowWidget } catch (Exception e) { + if (e is OperationCanceledException) + return; + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); Service.Get().AddNotification( $"Failed to save file: {e}", diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 39e969fb8..a9b393d3a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -45,13 +45,11 @@ internal sealed partial class FontAtlasFactory DataManager dataManager, Framework framework, InterfaceManager interfaceManager, - DalamudAssetManager dalamudAssetManager, - TextureManager textureManager) + DalamudAssetManager dalamudAssetManager) { this.Framework = framework; this.InterfaceManager = interfaceManager; this.dalamudAssetManager = dalamudAssetManager; - this.TextureManager = textureManager; this.SceneTask = Service .GetAsync() .ContinueWith(r => r.Result.Manager.Scene); @@ -148,7 +146,7 @@ internal sealed partial class FontAtlasFactory /// /// Gets the service instance of . /// - public TextureManager TextureManager { get; } + public TextureManager TextureManager => Service.Get(); /// /// Gets the async task for inside . diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 4735c1af7..9e6af982d 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -28,6 +28,8 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); - return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); + var wrap = await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index da3cc1a8d..e33091127 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -37,6 +37,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - return tm.NoThrottleCreateFromTexFile(file); + var wrap = tm.NoThrottleCreateFromTexFile(file); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index 00e2f34d5..525e25159 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -40,6 +40,8 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? checked((int)stream.Length) : 0); await stream.CopyToAsync(ms, cancellationToken); - return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); + var wrap = tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs new file mode 100644 index 000000000..a401a9a73 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -0,0 +1,373 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private readonly List blameTracker = new(); + + /// A wrapper for underlying texture2D resources. + public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap + { + /// Gets the name of the underlying resource of this texture wrap. + public string Name { get; } + + /// Gets the format of the texture. + public DXGI_FORMAT Format { get; } + + /// Gets the list of owner plugins. + public List OwnerPlugins { get; } + } + + /// Gets all the loaded textures from plugins. + /// The enumerable that goes through all textures and relevant plugins. + /// Returned value must be used inside a lock. + [SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Caller locks the return value.")] + public IReadOnlyList AllBlamesForDebug => this.blameTracker; + + /// Puts a plugin on blame for a texture. + /// The texture. + /// The plugin. + /// Same . + public unsafe IDalamudTextureWrap Blame(IDalamudTextureWrap textureWrap, LocalPlugin? ownerPlugin) + { + if (!this.dalamudConfiguration.UseTexturePluginTracking) + return textureWrap; + + using var wrapAux = new WrapAux(textureWrap, true); + var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + + if (ownerPlugin is not null) + { + lock (blame.OwnerPlugins) + blame.OwnerPlugins.Add(ownerPlugin); + } + + if (isNew) + { + lock (this.blameTracker) + this.blameTracker.Add(blame); + } + + return textureWrap; + } + + /// Sets the blame name for a texture. + /// The texture. + /// The name. + /// Same . + public unsafe IDalamudTextureWrap BlameSetName(IDalamudTextureWrap textureWrap, string name) + { + if (!this.dalamudConfiguration.UseTexturePluginTracking) + return textureWrap; + + using var wrapAux = new WrapAux(textureWrap, true); + var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + blame.Name = name; + + if (isNew) + { + lock (this.blameTracker) + this.blameTracker.Add(blame); + } + + return textureWrap; + } + + private void BlameTrackerUpdate(IFramework unused) + { + lock (this.blameTracker) + { + for (var i = 0; i < this.blameTracker.Count;) + { + var entry = this.blameTracker[i]; + if (entry.TestIsReleasedOrShouldRelease()) + { + this.blameTracker[i] = this.blameTracker[^1]; + this.blameTracker.RemoveAt(this.blameTracker.Count - 1); + } + else + { + ++i; + } + } + } + } + + /// A COM object that works by tagging itself to a DirectX resource. When the resource destructs, it will + /// also release our instance of the tag, letting us know that it is no longer being used, and can be evicted from + /// our tracker. + [Guid("2c3809e4-4f22-4c50-abde-4f22e5120875")] + private sealed unsafe class BlameTag : IUnknown.Interface, IRefCountable, IBlameableDalamudTextureWrap + { + private static readonly Guid MyGuid = typeof(BlameTag).GUID; + + private readonly nint[] comObject; + private readonly IUnknown.Vtbl vtbl; + private readonly D3D11_TEXTURE2D_DESC desc; + + private ID3D11Texture2D* tex2D; + private GCHandle gchThis; + private GCHandle gchComObject; + private GCHandle gchVtbl; + private int refCount; + + private ComPtr srvDebugPreview; + private long srvDebugPreviewExpiryTick; + + private BlameTag(IUnknown* trackWhat) + { + try + { + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + fixed (ID3D11Texture2D** ppTex2D = &this.tex2D) + trackWhat->QueryInterface(piid, (void**)ppTex2D).ThrowOnError(); + + fixed (D3D11_TEXTURE2D_DESC* pDesc = &this.desc) + this.tex2D->GetDesc(pDesc); + + this.comObject = new nint[2]; + + this.vtbl.QueryInterface = &QueryInterfaceStatic; + this.vtbl.AddRef = &AddRefStatic; + this.vtbl.Release = &ReleaseStatic; + + this.gchThis = GCHandle.Alloc(this); + this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned); + this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned); + this.comObject[0] = this.gchVtbl.AddrOfPinnedObject(); + this.comObject[1] = GCHandle.ToIntPtr(this.gchThis); + this.refCount = 1; + } + catch + { + this.refCount = 0; + if (this.gchComObject.IsAllocated) + this.gchComObject.Free(); + if (this.gchVtbl.IsAllocated) + this.gchVtbl.Free(); + if (this.gchThis.IsAllocated) + this.gchThis.Free(); + this.tex2D->Release(); + throw; + } + + try + { + fixed (Guid* pMyGuid = &MyGuid) + this.tex2D->SetPrivateDataInterface(pMyGuid, this).ThrowOnError(); + } + finally + { + // We don't own this. + this.tex2D->Release(); + + // If the try block above failed, then we will dispose ourselves right away. + // Otherwise, we are transferring our ownership to the device child tagging system. + this.Release(); + } + + return; + + [UnmanagedCallersOnly] + static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; + + [UnmanagedCallersOnly] + static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); + + [UnmanagedCallersOnly] + static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); + } + + /// + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + /// + public List OwnerPlugins { get; } = new(); + + /// + public string Name { get; set; } = ""; + + /// + public DXGI_FORMAT Format => this.desc.Format; + + /// + public IntPtr ImGuiHandle + { + get + { + if (this.refCount == 0) + return Service.Get().Empty4X4.ImGuiHandle; + + this.srvDebugPreviewExpiryTick = Environment.TickCount64 + 1000; + if (!this.srvDebugPreview.IsEmpty()) + return (nint)this.srvDebugPreview.Get(); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + this.tex2D, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + + using var device = default(ComPtr); + this.tex2D->GetDevice(device.GetAddressOf()); + + using var srv = default(ComPtr); + if (device.Get()->CreateShaderResourceView((ID3D11Resource*)this.tex2D, &srvDesc, srv.GetAddressOf()) + .FAILED) + return Service.Get().Empty4X4.ImGuiHandle; + + srv.Swap(ref this.srvDebugPreview); + return (nint)this.srvDebugPreview.Get(); + } + } + + /// + public int Width => (int)this.desc.Width; + + /// + public int Height => (int)this.desc.Height; + + public static implicit operator IUnknown*(BlameTag bt) => (IUnknown*)bt.gchComObject.AddrOfPinnedObject(); + + /// Gets or creates an instance of for the given resource. + /// The COM object to track. + /// true if the tracker is new. + /// A COM object type. + /// A new instance of . + public static BlameTag From(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface + { + using var deviceChild = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11DeviceChild) + trackWhat->QueryInterface(piid, (void**)deviceChild.GetAddressOf()).ThrowOnError(); + + fixed (Guid* pMyGuid = &MyGuid) + { + var dataSize = (uint)sizeof(nint); + IUnknown* existingTag; + if (deviceChild.Get()->GetPrivateData(pMyGuid, &dataSize, &existingTag).SUCCEEDED) + { + if (ToManagedObject(existingTag) is { } existingTagInstance) + { + existingTagInstance.Release(); + isNew = false; + return existingTagInstance; + } + } + } + + isNew = true; + return new((IUnknown*)trackWhat); + } + + /// Tests whether the tag and the underlying resource are released or should be released. + /// true if there are no more remaining references to this instance. + public bool TestIsReleasedOrShouldRelease() + { + if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64) + this.srvDebugPreview.Reset(); + + return this.refCount == 0; + } + + /// + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + if (ppvObject == null) + return E.E_POINTER; + + if (*riid == IID.IID_IUnknown || + *riid == MyGuid) + { + try + { + this.AddRef(); + } + catch + { + return E.E_FAIL; + } + + *ppvObject = (IUnknown*)this; + return S.S_OK; + } + + *ppvObject = null; + return E.E_NOINTERFACE; + } + + /// + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(BlameTag)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; + + /// + public int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) + { + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + this.gchThis.Free(); + this.gchComObject.Free(); + this.gchVtbl.Free(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(BlameTag)); + + default: + throw new InvalidOperationException(); + } + } + + /// + uint IUnknown.Interface.AddRef() + { + try + { + return (uint)this.AddRef(); + } + catch + { + return 0; + } + } + + /// + uint IUnknown.Interface.Release() + { + this.srvDebugPreviewExpiryTick = 0; + try + { + return (uint)this.Release(); + } + catch + { + return 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BlameTag? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as BlameTag; + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 3f0b69b96..41829f88c 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -71,23 +72,34 @@ internal sealed partial class TextureManager var desc = default(D3D11_TEXTURE2D_DESC); tex.Get()->GetDesc(&desc); - return new UnknownTextureWrap( + + var outWrap = new UnknownTextureWrap( (IUnknown*)srv.Get(), (int)desc.Width, (int)desc.Height, true); + this.BlameSetName( + outWrap, + $"{nameof(this.CreateFromExistingTextureAsync)}({nameof(wrap)}, {nameof(args)}, {nameof(leaveWrapOpen)}, {nameof(cancellationToken)})"); + return outWrap; } }, cancellationToken, leaveWrapOpen ? null : wrap); /// + Task ITextureProvider.CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, + CancellationToken cancellationToken) => this.CreateFromImGuiViewportAsync(args, null, cancellationToken); + + /// public Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, CancellationToken cancellationToken = default) { args.ThrowOnInvalidValues(); - var t = new ViewportTextureWrap(args, cancellationToken); + var t = new ViewportTextureWrap(args, ownerPlugin, cancellationToken); t.QueueUpdate(); return t.FirstUpdateTask; } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 25f3c634e..6d631a8ec 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Threading; @@ -11,7 +9,6 @@ using Dalamud.Game; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -64,6 +61,8 @@ internal sealed partial class TextureManager failsafe.Add(this.sharedTextureManager = new(this)); failsafe.Add(this.wicManager = new(this)); failsafe.Add(this.simpleDrawer = new()); + this.framework.Update += this.BlameTrackerUpdate; + failsafe.Add(() => this.framework.Update -= this.BlameTrackerUpdate); this.simpleDrawer.Setup(this.device.Get()); failsafe.Cancel(); @@ -116,21 +115,18 @@ internal sealed partial class TextureManager GC.SuppressFinalize(this); } - /// Puts a plugin on blame for a texture. - /// The texture. - /// The plugin. - public void Blame(IDalamudTextureWrap textureWrap, LocalPlugin ownerPlugin) - { - // nop for now - } - /// public Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, - ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), + ct => Task.Run( + () => + this.BlameSetName( + this.NoThrottleCreateFromImage(bytes.ToArray(), ct), + $"{nameof(this.CreateFromImageAsync)}({nameof(bytes)}, {nameof(cancellationToken)})"), + ct), cancellationToken); /// @@ -144,7 +140,9 @@ internal sealed partial class TextureManager { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct); + return this.BlameSetName( + this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), + $"{nameof(this.CreateFromImageAsync)}({nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, leaveOpen ? null : stream); @@ -153,7 +151,10 @@ internal sealed partial class TextureManager // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); + ReadOnlySpan bytes) => + this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, bytes), + $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); /// public Task CreateFromRawAsync( @@ -162,7 +163,10 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, - _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), + _ => Task.FromResult( + this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, bytes.Span), + $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(bytes)}, {nameof(cancellationToken)})")), cancellationToken); /// @@ -177,13 +181,18 @@ internal sealed partial class TextureManager { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); + return this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), + $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, leaveOpen ? null : stream); /// - public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => + this.BlameSetName( + this.CreateFromTexFileAsync(file).Result, + $"{nameof(this.CreateFromTexFile)}({nameof(file)})"); /// public Task CreateFromTexFileAsync( @@ -191,7 +200,10 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, - _ => Task.FromResult(this.NoThrottleCreateFromTexFile(file)), + _ => Task.FromResult( + this.BlameSetName( + this.NoThrottleCreateFromTexFile(file), + $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), cancellationToken); /// @@ -244,7 +256,9 @@ internal sealed partial class TextureManager this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) .ThrowOnError(); - return new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + return wrap; } /// Creates a texture from the given . Skips the load throttler; intended to be used @@ -264,9 +278,9 @@ internal sealed partial class TextureManager buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } - return this.NoThrottleCreateFromRaw( - new(buffer.Width, buffer.Height, dxgiFormat), - buffer.RawData); + var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(file)})"); + return wrap; } /// Creates a texture from the given , trying to interpret it as a @@ -289,7 +303,10 @@ internal sealed partial class TextureManager tf, new object?[] { new LuminaBinaryReader(bytesArray) }); // Note: FileInfo and FilePath are not used from TexFile; skip it. - return this.NoThrottleCreateFromTexFile(tf); + + var wrap = this.NoThrottleCreateFromTexFile(tf); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(fileBytes)})"); + return wrap; } private void ReleaseUnmanagedResources() => this.device.Reset(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 6ec346c30..8971409a7 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -40,8 +40,10 @@ internal sealed partial class TextureManagerPluginScoped private Task? managerTaskNullable; + /// Initializes a new instance of the class. + /// The plugin. [ServiceManager.ServiceConstructor] - private TextureManagerPluginScoped(LocalPlugin plugin) + public TextureManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; if (plugin.Manifest is LocalPluginManifest lpm) @@ -155,7 +157,7 @@ internal sealed partial class TextureManagerPluginScoped CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromImGuiViewportAsync(args, cancellationToken); + var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index a8692b323..daa247170 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -18,6 +20,7 @@ namespace Dalamud.Interface.Textures.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable { + private readonly LocalPlugin? ownerPlugin; private readonly CancellationToken cancellationToken; private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); @@ -31,10 +34,13 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos /// Initializes a new instance of the class. /// The arguments for creating a texture. + /// The owner plugin. /// The cancellation token. - public ViewportTextureWrap(ImGuiViewportTextureArgs args, CancellationToken cancellationToken) + public ViewportTextureWrap( + ImGuiViewportTextureArgs args, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) { this.args = args; + this.ownerPlugin = ownerPlugin; this.cancellationToken = cancellationToken; } @@ -42,7 +48,14 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos ~ViewportTextureWrap() => this.Dispose(false); /// - public unsafe nint ImGuiHandle => (nint)this.srv.Get(); + public unsafe nint ImGuiHandle + { + get + { + var t = (nint)this.srv.Get(); + return t == nint.Zero ? Service.Get().Empty4X4.ImGuiHandle : t; + } + } /// public int Width => (int)this.desc.Width; @@ -134,6 +147,11 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos srvTemp.Swap(ref this.srv); rtvTemp.Swap(ref this.rtv); texTemp.Swap(ref this.tex); + + Service.Get().Blame(this, this.ownerPlugin); + Service.Get().BlameSetName( + this, + $"{nameof(ViewportTextureWrap)}({this.args})"); } // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 8874c85f0..3b6a754a9 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -42,6 +42,7 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly TextureManagerPluginScoped scopedTextureProvider; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -54,8 +55,9 @@ public sealed class UiBuilder : IDisposable /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. /// + /// The plugin. /// The plugin namespace. - internal UiBuilder(string namespaceName) + internal UiBuilder(LocalPlugin plugin, string namespaceName) { try { @@ -69,6 +71,8 @@ public sealed class UiBuilder : IDisposable this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + this.scopedFinalizer.Add(this.scopedTextureProvider = new(plugin)); + this.FontAtlas = this.scopedFinalizer .Add( @@ -381,8 +385,6 @@ public sealed class UiBuilder : IDisposable private Task InterfaceManagerWithSceneAsync => Service.GetAsync().ContinueWith(task => task.Result.Manager); - private ITextureProvider TextureProvider => Service.Get(); - /// /// Loads an image from the specified file. /// @@ -391,7 +393,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] public IDalamudTextureWrap LoadImage(string filePath) => - this.TextureProvider.GetFromFile(filePath).RentAsync().Result; + this.scopedTextureProvider.GetFromFile(filePath).RentAsync().Result; /// /// Loads an image from a byte stream, such as a png downloaded into memory. @@ -401,7 +403,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] public IDalamudTextureWrap LoadImage(byte[] imageData) => - this.TextureProvider.CreateFromImageAsync(imageData).Result; + this.scopedTextureProvider.CreateFromImageAsync(imageData).Result; /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . @@ -416,7 +418,7 @@ public sealed class UiBuilder : IDisposable public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.scopedTextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), _ => throw new NotSupportedException(), }; @@ -436,7 +438,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] public Task LoadImageAsync(string filePath) => - this.TextureProvider.GetFromFile(filePath).RentAsync(); + this.scopedTextureProvider.GetFromFile(filePath).RentAsync(); /// /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. @@ -446,7 +448,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] public Task LoadImageAsync(byte[] imageData) => - this.TextureProvider.CreateFromImageAsync(imageData); + this.scopedTextureProvider.CreateFromImageAsync(imageData); /// /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . @@ -461,7 +463,7 @@ public sealed class UiBuilder : IDisposable public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.scopedTextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), _ => Task.FromException(new NotSupportedException()), }; diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa49..2b184288d 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(plugin.Name); + this.UiBuilder = new(plugin, plugin.Name); this.configs = Service.Get().PluginConfigs; this.Reason = reason; diff --git a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs index 942a9baf3..caec65da2 100644 --- a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -54,64 +54,64 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable return; [MethodImpl(MethodImplOptions.AggressiveInlining)] - static IStream.Interface? ToManagedObject(void* pThis) => - GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as IStream.Interface; + static ManagedIStream? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream; [UnmanagedCallersOnly] static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => - ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_FAIL; + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static uint AddRefStatic(IStream* pThis) => ToManagedObject(pThis)?.AddRef() ?? 0; + static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); [UnmanagedCallersOnly] - static uint ReleaseStatic(IStream* pThis) => ToManagedObject(pThis)?.Release() ?? 0; + static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); [UnmanagedCallersOnly] static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => - ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_FAIL; + ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => - ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_FAIL; + ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int SeekStatic( IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => - ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_FAIL; + ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => - ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_FAIL; + ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int CopyToStatic( IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) => - ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_FAIL; + ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int CommitStatic(IStream* pThis, uint grfCommitFlags) => - ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_FAIL; + ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_FAIL; + static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => - ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int UnlockRegionStatic( IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => - ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => - ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_FAIL; + ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_FAIL; + static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; } ///