diff --git a/Dalamud/Interface/GameIconLookup.cs b/Dalamud/Interface/GameIconLookup.cs deleted file mode 100644 index 001f519aa..000000000 --- a/Dalamud/Interface/GameIconLookup.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Interface; - -/// -/// Represents a lookup for a game icon. -/// -/// The icon ID. -/// Whether the HQ icon is requested, where HQ is in the context of items. -/// Whether the high-resolution icon is requested. -/// The language of the icon to load. -[SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1313:Parameter names should begin with lower-case letter", - Justification = "no")] -public record struct GameIconLookup( - uint IconId, - bool ItemHq = false, - bool HiRes = true, - ClientLanguage? Language = null); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 00bef19af..aab4996bb 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading.Tasks; using CheapLoc; using Dalamud.Configuration.Internal; @@ -15,7 +18,9 @@ using Dalamud.Game.Internal; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.ManagedAsserts; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -24,6 +29,7 @@ using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -40,6 +46,10 @@ using ImPlotNET; using PInvoke; using Serilog.Events; +using TerraFX.Interop.Windows; + +using Task = System.Threading.Tasks.Task; + namespace Dalamud.Interface.Internal; /// @@ -56,6 +66,8 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly DalamudConfiguration configuration; private readonly InterfaceManager interfaceManager; + private readonly FileDialogManager fileDialogManager; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -109,6 +121,7 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); + this.fileDialogManager = new(); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; @@ -486,6 +499,119 @@ internal class DalamudInterface : IDisposable, IServiceType this.creditsDarkeningAnimation.Restart(); } + /// Shows a context menu confirming texture save. + /// Name of the initiator. + /// Suggested name of the file being saved. + /// A task returning the texture to save. + /// A representing the asynchronous operation. + public async Task ShowTextureSaveMenuAsync( + string initiatorName, + string name, + Task texture) + { + try + { + var initiatorScreenOffset = ImGui.GetMousePos(); + using var textureWrap = await texture; + var textureManager = await Service.GetAsync(); + var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; + + BitmapCodecInfo encoder; + { + var first = true; + var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); + var tcs = new TaskCompletionSource(); + Service.Get().Draw += DrawChoices; + + encoder = await tcs.Task; + + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] + void DrawChoices() + { + if (first) + { + ImGui.OpenPopup(popupName); + first = false; + } + + ImGui.SetNextWindowPos(initiatorScreenOffset, ImGuiCond.Appearing); + if (!ImGui.BeginPopup( + popupName, + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoSavedSettings)) + { + Service.Get().Draw -= DrawChoices; + tcs.TrySetCanceled(); + return; + } + + foreach (var encoder2 in encoders) + { + if (ImGui.Selectable(encoder2.Name)) + tcs.TrySetResult(encoder2); + } + + const float previewImageWidth = 320; + var size = textureWrap.Size; + if (size.X > previewImageWidth) + size *= previewImageWidth / size.X; + if (size.Y > previewImageWidth) + size *= previewImageWidth / size.Y; + ImGui.Image(textureWrap.ImGuiHandle, size); + + if (tcs.Task.IsCompleted) + ImGui.CloseCurrentPopup(); + + ImGui.EndPopup(); + } + } + + string path; + { + var tcs = new TaskCompletionSource(); + this.fileDialogManager.SaveFileDialog( + "Save texture...", + $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", + name + encoder.Extensions.First(), + encoder.Extensions.First(), + (ok, path2) => + { + if (!ok) + tcs.SetCanceled(); + else + tcs.SetResult(path2); + }); + path = await tcs.Task.ConfigureAwait(false); + } + + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + + Service.Get().AddNotification( + $"File saved to: {path}", + initiatorName, + NotificationType.Success); + } + catch (Exception e) + { + if (e is OperationCanceledException) + return; + + Log.Error(e, $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + initiatorName, + NotificationType.Error); + } + } + private void OnDraw() { this.FrameCount++; @@ -537,6 +663,8 @@ internal class DalamudInterface : IDisposable, IServiceType { ImGui.SetWindowFocus(null); } + + this.fileDialogManager.Draw(); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 65c10d3a0..87bff1325 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -199,6 +199,10 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool IsDispatchingEvents { get; set; } = true; + /// Gets a value indicating whether the main thread is executing . + /// This still will be true even when queried off the main thread. + public bool IsInPresent { get; private set; } + /// /// Gets a value indicating the native handle of the game main window. /// @@ -595,8 +599,6 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { - this.CumulativePresentCalls++; - Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); @@ -611,6 +613,9 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + this.CumulativePresentCalls++; + this.IsInPresent = true; + while (this.runBeforeImGuiRender.TryDequeue(out var action)) action.InvokeSafely(); @@ -620,12 +625,14 @@ internal class InterfaceManager : IDisposable, IServiceType RenderImGui(this.scene!); this.PostImGuiRender(); + this.IsInPresent = false; return pRes; } RenderImGui(this.scene!); this.PostImGuiRender(); + this.IsInPresent = false; return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index b7b897e68..e10fca0e7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -142,7 +142,7 @@ public class IconBrowserWidget : IDataWindowWidget var texm = Service.Get(); var cursor = ImGui.GetCursorScreenPos(); - if (texm.Shared.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc)) + if (texm.Shared.GetFromGameIcon(iconId).TryGetWrap(out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); @@ -168,6 +168,14 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.SetTooltip(iconId.ToString()); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + iconId.ToString(), + Task.FromResult(texture.CreateWrapSharingLowLevelResource())); + } + ImGui.GetWindowDrawList().AddRect( cursor, cursor + this.iconSize, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 58a2b6b6e..4713022e6 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -8,10 +8,8 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; @@ -20,10 +18,7 @@ using Dalamud.Utility; using ImGuiNET; -using Serilog; - using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -34,8 +29,22 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { + private static readonly Dictionary< + DrawBlameTableColumnUserId, + Func> DrawBlameTableColumnColumnComparers = new() + { + [DrawBlameTableColumnUserId.Plugins] = static x => string.Join(", ", x.OwnerPlugins.Select(y => y.Name)), + [DrawBlameTableColumnUserId.Name] = static x => x.Name, + [DrawBlameTableColumnUserId.Size] = static x => x.RawSpecs.EstimatedBytes, + [DrawBlameTableColumnUserId.Format] = static x => x.Format, + [DrawBlameTableColumnUserId.Width] = static x => x.Width, + [DrawBlameTableColumnUserId.Height] = static x => x.Height, + [DrawBlameTableColumnUserId.NativeAddress] = static x => x.ResourceAddress, + }; + private readonly List addedTextures = new(); + private string allLoadedTexturesTableName = "##table"; private string iconId = "18"; private bool hiRes = true; private bool hq = false; @@ -51,7 +60,6 @@ internal class TexWidget : IDataWindowWidget private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; - private FileDialogManager fileDialogManager = null!; private TextureModificationArgs textureModificationArgs; private ImGuiViewportTextureArgs viewportTextureArgs; @@ -60,6 +68,19 @@ internal class TexWidget : IDataWindowWidget private DXGI_FORMAT[]? supportedRenderTargetFormats; private int renderTargetChoiceInt; + private enum DrawBlameTableColumnUserId + { + NativeAddress, + Actions, + Name, + Width, + Height, + Format, + Size, + Plugins, + ColumnCount, + } + /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; @@ -74,6 +95,7 @@ internal class TexWidget : IDataWindowWidget /// public void Load() { + this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; this.addedTextures.AggregateToDisposable().Dispose(); this.addedTextures.Clear(); this.inputTexPath = "ui/loadingimage/-nowloading_base25_hr1.tex"; @@ -88,7 +110,6 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; this.renderTargetChoiceInt = 0; - this.fileDialogManager = new(); this.textureModificationArgs = new() { Uv0 = new(0.25f), @@ -105,30 +126,44 @@ internal class TexWidget : IDataWindowWidget public void Draw() { this.textureManager = Service.Get(); + var conf = Service.Get(); 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(); + var useTexturePluginTracking = conf.UseTexturePluginTracking; + if (ImGui.Checkbox("Enable Texture Tracking", ref useTexturePluginTracking)) + { + conf.UseTexturePluginTracking = useTexturePluginTracking; + conf.QueueSave(); + } + + var allBlames = this.textureManager.BlameTracker; + lock (allBlames) + { + ImGui.PushID("blames"); + var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes)); + if (ImGui.CollapsingHeader( + $"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header")) + this.DrawBlame(allBlames); + ImGui.PopID(); + } ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( - $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header")) + $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); if (ImGui.CollapsingHeader( - $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:g}###header")) + $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures); ImGui.PopID(); ImGui.PushID("loadedManifestResourceTextures"); if (ImGui.CollapsingHeader( - $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:g}###header")) + $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); ImGui.PopID(); @@ -136,7 +171,7 @@ internal class TexWidget : IDataWindowWidget { ImGui.PushID("invalidatedTextures"); if (ImGui.CollapsingHeader( - $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:g}###header")) + $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header")) { this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures); } @@ -215,9 +250,10 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Save")) { - this.SaveTextureAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, $"Texture {t.Id}", - () => t.CreateNewTextureWrapReference(this.textureManager)); + t.CreateNewTextureWrapReference(this.textureManager)); } ImGui.SameLine(); @@ -262,10 +298,12 @@ internal class TexWidget : IDataWindowWidget pres->Release(); ImGui.TextUnformatted($"RC: Resource({rcres})/View({rcsrv})"); + ImGui.TextUnformatted(source.ToString()); } else { ImGui.TextUnformatted("RC: -"); + ImGui.TextUnformatted(" "); } } @@ -294,22 +332,24 @@ internal class TexWidget : IDataWindowWidget } runLater?.Invoke(); - - this.fileDialogManager.Draw(); } - private unsafe void DrawBlame(IReadOnlyList allBlames) + private unsafe void DrawBlame(List 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", 6)) + var shouldSortAgain = ImGui.Button("Sort again"); + + ImGui.SameLine(); + if (ImGui.Button("Reset Columns")) + this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; + + if (!ImGui.BeginTable( + this.allLoadedTexturesTableName, + (int)DrawBlameTableColumnUserId.ColumnCount, + ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti | + ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize | + ImGuiTableFlags.NoSavedSettings)) return; const int numIcons = 1; @@ -319,32 +359,88 @@ internal class TexWidget : IDataWindowWidget ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn( - "Dimensions", + "Address", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("00000x00000").X); + ImGui.CalcTextSize("0x7F0000000000").X, + (uint)DrawBlameTableColumnUserId.NativeAddress); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons), + (uint)DrawBlameTableColumnUserId.Actions); + ImGui.TableSetupColumn( + "Name", + ImGuiTableColumnFlags.WidthStretch, + 0f, + (uint)DrawBlameTableColumnUserId.Name); + ImGui.TableSetupColumn( + "Width", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("000000").X, + (uint)DrawBlameTableColumnUserId.Width); + ImGui.TableSetupColumn( + "Height", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("000000").X, + (uint)DrawBlameTableColumnUserId.Height); ImGui.TableSetupColumn( "Format", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X); + ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X, + (uint)DrawBlameTableColumnUserId.Format); ImGui.TableSetupColumn( "Size", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("123.45 MB").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.CalcTextSize("123.45 MB").X, + (uint)DrawBlameTableColumnUserId.Size); ImGui.TableSetupColumn( "Plugins", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X); + ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X, + (uint)DrawBlameTableColumnUserId.Plugins); ImGui.TableHeadersRow(); + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.NativePtr is not null && (sortSpecs.SpecsDirty || shouldSortAgain)) + { + allBlames.Sort( + static (a, b) => + { + var sortSpecs = ImGui.TableGetSortSpecs(); + var specs = new Span(sortSpecs.NativePtr->Specs, sortSpecs.SpecsCount); + Span sorted = stackalloc bool[(int)DrawBlameTableColumnUserId.ColumnCount]; + foreach (ref var spec in specs) + { + if (!DrawBlameTableColumnColumnComparers.TryGetValue( + (DrawBlameTableColumnUserId)spec.ColumnUserID, + out var comparableGetter)) + continue; + sorted[(int)spec.ColumnUserID] = true; + var ac = comparableGetter(a); + var bc = comparableGetter(b); + var c = ac.CompareTo(bc); + if (c != 0) + return spec.SortDirection == ImGuiSortDirection.Ascending ? c : -c; + } + + foreach (var (col, comparableGetter) in DrawBlameTableColumnColumnComparers) + { + if (sorted[(int)col]) + continue; + var ac = comparableGetter(a); + var bc = comparableGetter(b); + var c = ac.CompareTo(bc); + if (c != 0) + return c; + } + + return 0; + }); + sortSpecs.SpecsDirty = false; + } + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); clipper.Begin(allBlames.Count); @@ -357,25 +453,16 @@ internal class TexWidget : IDataWindowWidget 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(); - var rawSpec = new RawImageSpecification(wrap.Width, wrap.Height, (int)wrap.Format, 0); - var bytes = rawSpec.EstimatedBytes; - ImGui.TextUnformatted(bytes < 0 ? "-" : Util.FormatBytes(bytes)); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(wrap.Name); + ImGui.AlignTextToFramePadding(); + this.TextCopiable($"0x{wrap.ResourceAddress:X}", true, true); ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { - this.SaveTextureAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, $"{wrap.ImGuiHandle:X16}", - () => Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); + Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); } if (ImGui.IsItemHovered()) @@ -385,12 +472,25 @@ internal class TexWidget : IDataWindowWidget ImGui.EndTooltip(); } + ImGui.TableNextColumn(); + this.TextCopiable(wrap.Name, false, true); + + ImGui.TableNextColumn(); + this.TextCopiable($"{wrap.Width:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable($"{wrap.Height:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString(), false, true); + + ImGui.TableNextColumn(); + var bytes = wrap.RawSpecs.EstimatedBytes; + this.TextCopiable(bytes < 0 ? "?" : $"{bytes:n0}", true, true); + ImGui.TableNextColumn(); lock (wrap.OwnerPlugins) - { - foreach (var plugin in wrap.OwnerPlugins) - ImGui.TextUnformatted(plugin.Name); - } + this.TextCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); ImGui.PopID(); } @@ -468,16 +568,16 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - this.TextRightAlign($"{texture.InstanceIdForDebug:n0}"); + this.TextCopiable($"{texture.InstanceIdForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable(texture.SourcePathForDebug, true); + this.TextCopiable(texture.SourcePathForDebug, false, true); ImGui.TableNextColumn(); - this.TextRightAlign($"{texture.RefCountForDebug:n0}"); + this.TextCopiable($"{texture.RefCountForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); + this.TextCopiable(remain <= 0 ? "-" : $"{remain:00.000}", true, true); ImGui.TableNextColumn(); ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); @@ -486,7 +586,10 @@ internal class TexWidget : IDataWindowWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null); - this.SaveTextureAsync(name, () => texture.RentAsync()); + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + name, + texture.RentAsync()); } if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) @@ -783,119 +886,24 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private async void SaveTextureAsync(string name, Func> textureGetter) - { - try - { - BitmapCodecInfo encoder; - { - var off = ImGui.GetCursorScreenPos(); - var first = true; - var encoders = this.textureManager - .Wic - .GetSupportedEncoderInfos() - .ToList(); - var tcs = new TaskCompletionSource(); - Service.Get().Draw += DrawChoices; - - encoder = await tcs.Task; - - void DrawChoices() - { - if (first) - { - ImGui.OpenPopup(nameof(this.SaveTextureAsync)); - first = false; - } - - ImGui.SetNextWindowPos(off, ImGuiCond.Appearing); - if (!ImGui.BeginPopup( - nameof(this.SaveTextureAsync), - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoSavedSettings)) - { - Service.Get().Draw -= DrawChoices; - tcs.TrySetCanceled(); - return; - } - - foreach (var encoder2 in encoders) - { - if (ImGui.Selectable(encoder2.Name)) - tcs.TrySetResult(encoder2); - } - - if (tcs.Task.IsCompleted) - ImGui.CloseCurrentPopup(); - - ImGui.EndPopup(); - } - } - - string path; - { - var tcs = new TaskCompletionSource(); - this.fileDialogManager.SaveFileDialog( - "Save texture...", - $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", - name + encoder.Extensions.First(), - encoder.Extensions.First(), - (ok, path2) => - { - if (!ok) - tcs.SetCanceled(); - else - tcs.SetResult(path2); - }); - path = await tcs.Task.ConfigureAwait(false); - } - - using var textureWrap = await textureGetter.Invoke(); - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await this.textureManager.SaveToFileAsync( - textureWrap, - encoder.ContainerGuid, - path, - props: props); - - Service.Get().AddNotification( - $"File saved to: {path}", - this.DisplayName, - NotificationType.Success); - } - catch (Exception e) - { - if (e is OperationCanceledException) - return; - - Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - this.DisplayName, - NotificationType.Error); - } - } - - private void TextRightAlign(string s) - { - var width = ImGui.CalcTextSize(s).X; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetColumnWidth() - width); - ImGui.TextUnformatted(s); - } - - private void TextCopiable(string s, bool framepad = false) + private void TextCopiable(string s, bool alignRight, bool framepad) { var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); if (framepad) ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(s); + if (alignRight) + { + var width = ImGui.CalcTextSize(s).X; + var xoff = ImGui.GetColumnWidth() - width; + offset.X += xoff; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xoff); + ImGui.TextUnformatted(s); + } + else + { + ImGui.TextUnformatted(s); + } + if (ImGui.IsItemHovered()) { ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index f091e3164..d2e513c1b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -591,7 +591,7 @@ internal sealed partial class FontAtlasFactory { ref var texture = ref textureSpan[i]; var name = - $"FontAtlas.{ this.data.Owner?.Name ?? "(no owner or name)"}[0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; + $"{nameof(FontAtlasBuiltData)}[{this.data.Owner?.Name ?? "-"}][0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; if (texture.TexID != 0) { // Nothing to do @@ -602,6 +602,7 @@ internal sealed partial class FontAtlasFactory RawImageSpecification.Rgba32(width, height), new(texture.TexPixelsRGBA32, width * height * 4), name); + this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -647,6 +648,7 @@ internal sealed partial class FontAtlasFactory width * bpp), buf, name); + this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 883fcbbfc..9a6520e25 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; @@ -256,12 +257,15 @@ internal sealed partial class FontAtlasFactory /// Name of atlas, for debugging and logging purposes. /// Specify how to auto rebuild. /// Whether the fonts in the atlas are under the effect of global scale. + /// The owner plugin, if any. public DalamudFontAtlas( FontAtlasFactory factory, string atlasName, FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled) + bool isGlobalScaled, + LocalPlugin? ownerPlugin) { + this.OwnerPlugin = ownerPlugin; this.IsGlobalScaled = isGlobalScaled; try { @@ -355,6 +359,9 @@ internal sealed partial class FontAtlasFactory /// public bool IsGlobalScaled { get; } + /// Gets the owner plugin, if any. + public LocalPlugin? OwnerPlugin { get; } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 59df710d6..aaf031cba 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -178,12 +179,14 @@ internal sealed partial class FontAtlasFactory /// Name of atlas, for debugging and logging purposes. /// Specify how to auto rebuild. /// Whether the fonts in the atlas is global scaled. + /// The owner plugin, if any. /// The new font atlas. public IFontAtlas CreateFontAtlas( string atlasName, FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true) => - new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + bool isGlobalScaled = true, + LocalPlugin? ownerPlugin = null) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled, ownerPlugin); /// /// Adds the font from Dalamud Assets. @@ -363,7 +366,7 @@ internal sealed partial class FontAtlasFactory : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), texFile.Header.Width * bpp), buffer, - $"{nameof(FontAtlasFactory)}:{texPathFormat.Format(fileIndex)}:{channelIndex}")); + $"{nameof(FontAtlasFactory)}[{texPathFormat.Format(fileIndex)}][{channelIndex}]")); } finally { diff --git a/Dalamud/Interface/Textures/GameIconLookup.cs b/Dalamud/Interface/Textures/GameIconLookup.cs new file mode 100644 index 000000000..ccc999d56 --- /dev/null +++ b/Dalamud/Interface/Textures/GameIconLookup.cs @@ -0,0 +1,55 @@ +using System.Text; + +namespace Dalamud.Interface.Textures; + +/// Represents a lookup for a game icon. +public readonly record struct GameIconLookup +{ + /// Initializes a new instance of the class. + /// The icon ID. + /// Whether the HQ icon is requested, where HQ is in the context of items. + /// Whether the high-resolution icon is requested. + /// The language of the icon to load. + public GameIconLookup(uint iconId, bool itemHq = false, bool hiRes = true, ClientLanguage? language = null) + { + this.IconId = iconId; + this.ItemHq = itemHq; + this.HiRes = hiRes; + this.Language = language; + } + + public static implicit operator GameIconLookup(int iconId) => new(checked((uint)iconId)); + + public static implicit operator GameIconLookup(uint iconId) => new(iconId); + + /// Gets the icon ID. + public uint IconId { get; init; } + + /// Gets a value indicating whether the HQ icon is requested, where HQ is in the context of items. + public bool ItemHq { get; init; } + + /// Gets a value indicating whether the high-resolution icon is requested. + public bool HiRes { get; init; } + + /// Gets the language of the icon to load. + /// + /// null will use the active game language. + /// If the specified resource does not have variants per language, the language-neutral texture will be used. + /// + /// + public ClientLanguage? Language { get; init; } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(GameIconLookup)).Append('(').Append(this.IconId); + if (this.ItemHq) + sb.Append(", HQ"); + if (this.HiRes) + sb.Append(", HR1"); + if (this.Language is not null) + sb.Append(", ").Append(Enum.GetName(this.Language.Value)); + return sb.Append(')').ToString(); + } +} diff --git a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs index 8193d65e6..1159f5dbf 100644 --- a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -1,9 +1,12 @@ using System.Numerics; +using System.Text; using Dalamud.Interface.Internal; using ImGuiNET; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Textures; /// Describes how to take a texture of an existing ImGui viewport. @@ -44,6 +47,30 @@ public record struct ImGuiViewportTextureArgs() /// Gets the effective value of . internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(ImGuiViewportTextureArgs)).Append('('); + sb.Append($"0x{this.ViewportId:X}"); + if (this.AutoUpdate) + sb.Append($", {nameof(this.AutoUpdate)}"); + if (this.TakeBeforeImGuiRender) + sb.Append($", {nameof(this.TakeBeforeImGuiRender)}"); + if (this.KeepTransparency) + sb.Append($", {nameof(this.KeepTransparency)}"); + + if (this.Uv0 != Vector2.Zero || this.Uv1Effective != Vector2.One) + { + sb.Append(", ") + .Append(this.Uv0.ToString()) + .Append('-') + .Append(this.Uv1.ToString()); + } + + return sb.Append(')').ToString(); + } + /// Checks the properties and throws an exception if values are invalid. internal void ThrowOnInvalidValues() { diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 9e6af982d..69aca5c69 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -18,6 +18,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// Creates a new placeholder instance of . /// The path. /// The new instance. + /// Only to be used from . public static SharedImmediateTexture CreatePlaceholder(string path) => new FileSystemSharedImmediateTexture(path); /// diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index e33091127..8a1caacd6 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -22,6 +22,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// Creates a new placeholder instance of . /// The path. /// The new instance. + /// Only to be used from . public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path); /// diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index 525e25159..34ffbaf0e 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -27,6 +27,8 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// Creates a new placeholder instance of . /// The arguments to pass to the constructor. /// The new instance. + /// Only to be used from . + /// public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) => new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index ed76223a3..a20736e0a 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -6,6 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -22,6 +24,7 @@ internal abstract class SharedImmediateTexture private static long instanceCounter; private readonly object reviveLock = new(); + private readonly List ownerPlugins = new(); private bool resourceReleased; private int refCount; @@ -43,10 +46,11 @@ internal abstract class SharedImmediateTexture this.IsOpportunistic = true; this.resourceReleased = true; this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; + this.PublicUseInstance = new(this); } - /// Gets the list of owner plugins. - public List OwnerPlugins { get; } = new(); + /// Gets a wrapper for this instance which disables resource reference management. + public PureImpl PublicUseInstance { get; } /// Gets the instance ID. Debug use only. public long InstanceIdForDebug { get; } @@ -280,15 +284,15 @@ internal abstract class SharedImmediateTexture return this.availableOnAccessWrapForApi9; } - /// Adds a plugin to , in a thread-safe way. + /// Adds a plugin to , in a thread-safe way. /// The plugin to add. public void AddOwnerPlugin(LocalPlugin plugin) { - lock (this.OwnerPlugins) + lock (this.ownerPlugins) { - if (!this.OwnerPlugins.Contains(plugin)) + if (!this.ownerPlugins.Contains(plugin)) { - this.OwnerPlugins.Add(plugin); + this.ownerPlugins.Add(plugin); this.UnderlyingWrap?.ContinueWith( r => { @@ -314,14 +318,14 @@ internal abstract class SharedImmediateTexture protected void LoadUnderlyingWrap() { int addLen; - lock (this.OwnerPlugins) + lock (this.ownerPlugins) { this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); - addLen = this.OwnerPlugins.Count; + addLen = this.ownerPlugins.Count; } if (addLen == 0) @@ -331,8 +335,11 @@ internal abstract class SharedImmediateTexture { if (!r.IsCompletedSuccessfully) return; - foreach (var op in this.OwnerPlugins.Take(addLen)) - Service.Get().Blame(r.Result, op); + lock (this.ownerPlugins) + { + foreach (var op in this.ownerPlugins.Take(addLen)) + Service.Get().Blame(r.Result, op); + } }, default(CancellationToken)); } @@ -427,6 +434,52 @@ internal abstract class SharedImmediateTexture } } + /// A wrapper around , to prevent external consumers from mistakenly + /// calling or . + internal sealed class PureImpl : ISharedImmediateTexture + { + private readonly SharedImmediateTexture inner; + + /// Initializes a new instance of the class. + /// The actual instance. + public PureImpl(SharedImmediateTexture inner) => this.inner = inner; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap GetWrapOrEmpty() => + this.inner.GetWrapOrEmpty(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(defaultWrap))] + public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null) => + this.inner.GetWrapOrDefault(defaultWrap); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) => + this.inner.TryGetWrap(out texture, out exception); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task RentAsync(CancellationToken cancellationToken = default) => + this.inner.RentAsync(cancellationToken); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap? GetAvailableOnAccessWrapForApi9() => + this.inner.GetAvailableOnAccessWrapForApi9(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOwnerPlugin(LocalPlugin plugin) => + this.inner.AddOwnerPlugin(plugin); + + /// + public override string ToString() => $"{this.inner}({nameof(PureImpl)})"; + } + /// Same with , but with a custom implementation of /// . private sealed class NotOwnedTextureWrap : DisposeSuppressingTextureWrap diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index 5e47d2c8e..a306b7c64 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,11 +17,12 @@ 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 address of the native resource. + public nint ResourceAddress { get; } + /// Gets the name of the underlying resource of this texture wrap. public string Name { get; } @@ -31,13 +31,27 @@ internal sealed partial class TextureManager /// Gets the list of owner plugins. public List OwnerPlugins { get; } + + /// Gets the raw image specification. + public RawImageSpecification RawSpecs { get; } + + /// 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. + bool TestIsReleasedOrShouldRelease(); } - /// Gets all the loaded textures from plugins. - /// The enumerable that goes through all textures and relevant plugins. + /// Gets the list containing all the loaded textures from plugins. /// Returned value must be used inside a lock. - [SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Caller locks the return value.")] - public IReadOnlyList AllBlamesForDebug => this.blameTracker; + public List BlameTracker { get; } = new(); + + /// Gets the blame for a texture wrap. + /// The texture wrap. + /// The blame, if it exists. + public unsafe IBlameableDalamudTextureWrap? GetBlame(IDalamudTextureWrap textureWrap) + { + using var wrapAux = new WrapAux(textureWrap, true); + return BlameTag.Get(wrapAux.ResPtr); + } /// Puts a plugin on blame for a texture. /// The texture. @@ -59,7 +73,7 @@ internal sealed partial class TextureManager } using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); if (ownerPlugin is not null) { @@ -69,8 +83,8 @@ internal sealed partial class TextureManager if (isNew) { - lock (this.blameTracker) - this.blameTracker.Add(blame); + lock (this.BlameTracker) + this.BlameTracker.Add(blame); } return textureWrap; @@ -96,13 +110,13 @@ internal sealed partial class TextureManager } using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); - blame.Name = name; + var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); + blame.Name = name.Length <= 1024 ? name : $"{name[..1024]}..."; if (isNew) { - lock (this.blameTracker) - this.blameTracker.Add(blame); + lock (this.BlameTracker) + this.BlameTracker.Add(blame); } return textureWrap; @@ -110,15 +124,15 @@ internal sealed partial class TextureManager private void BlameTrackerUpdate(IFramework unused) { - lock (this.blameTracker) + lock (this.BlameTracker) { - for (var i = 0; i < this.blameTracker.Count;) + for (var i = 0; i < this.BlameTracker.Count;) { - var entry = this.blameTracker[i]; + var entry = this.BlameTracker[i]; if (entry.TestIsReleasedOrShouldRelease()) { - this.blameTracker[i] = this.blameTracker[^1]; - this.blameTracker.RemoveAt(this.blameTracker.Count - 1); + this.BlameTracker[i] = this.BlameTracker[^1]; + this.BlameTracker.RemoveAt(this.BlameTracker.Count - 1); } else { @@ -220,12 +234,22 @@ internal sealed partial class TextureManager /// public List OwnerPlugins { get; } = new(); + /// + public nint ResourceAddress => (nint)this.tex2D; + /// public string Name { get; set; } = ""; /// public DXGI_FORMAT Format => this.desc.Format; + /// + public RawImageSpecification RawSpecs => new( + (int)this.desc.Width, + (int)this.desc.Height, + (int)this.desc.Format, + 0); + /// public IntPtr ImGuiHandle { @@ -267,7 +291,23 @@ internal sealed partial class TextureManager /// 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 + public static BlameTag GetOrCreate(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface + { + if (Get(trackWhat) is { } v) + { + isNew = false; + return v; + } + + isNew = true; + return new((IUnknown*)trackWhat); + } + + /// Gets an existing instance of for the given resource. + /// The COM object to track. + /// A COM object type. + /// An existing instance of . + public static BlameTag? Get(T* trackWhat) where T : unmanaged, IUnknown.Interface { using var deviceChild = default(ComPtr); fixed (Guid* piid = &IID.IID_ID3D11DeviceChild) @@ -282,18 +322,15 @@ internal sealed partial class TextureManager if (ToManagedObject(existingTag) is { } existingTagInstance) { existingTagInstance.Release(); - isNew = false; return existingTagInstance; } } } - isNew = true; - return new((IUnknown*)trackWhat); + return null; } - /// 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) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 9d10457dc..7e9b209cf 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.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -82,7 +83,7 @@ internal sealed partial class TextureManager this.BlameSetName( outWrap, debugName ?? - $"{nameof(this.CreateFromExistingTextureAsync)}({nameof(wrap)}, {nameof(args)}, {nameof(leaveWrapOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromExistingTextureAsync)}({wrap}, {args})"); return outWrap; } }, @@ -136,59 +137,57 @@ internal sealed partial class TextureManager } cancellationToken.ThrowIfCancellationRequested(); - return await this.interfaceManager.RunBeforeImGuiRender( - () => ExtractMappedResource(wrapAux, tex2D, cancellationToken)); + + // ID3D11DeviceContext is not a threadsafe resource, and it must be used from the UI thread. + return await this.RunDuringPresent(() => ExtractMappedResource(tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( - in WrapAux wrapAux, ComPtr tex2D, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - ID3D11Resource* mapWhat = null; + D3D11_TEXTURE2D_DESC desc; + tex2D.Get()->GetDesc(&desc); + + using var device = default(ComPtr); + tex2D.Get()->GetDevice(device.GetAddressOf()); + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using var tmpTex = default(ComPtr); + if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) + { + var tmpTexDesc = desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); + context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); + + cancellationToken.ThrowIfCancellationRequested(); + } + + var mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); + + D3D11_MAPPED_SUBRESOURCE mapped; + context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError(); + try { - using var tmpTex = default(ComPtr); - if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) - { - var tmpTexDesc = wrapAux.Desc with - { - MipLevels = 1, - ArraySize = 1, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_STAGING, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - MiscFlags = 0u, - }; - wrapAux.DevPtr->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); - wrapAux.CtxPtr->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); - - cancellationToken.ThrowIfCancellationRequested(); - } - - D3D11_MAPPED_SUBRESOURCE mapped; - mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); - wrapAux.CtxPtr->Map( - mapWhat, - 0, - D3D11_MAP.D3D11_MAP_READ, - 0, - &mapped).ThrowOnError(); - - var specs = new RawImageSpecification( - (int)wrapAux.Desc.Width, - (int)wrapAux.Desc.Height, - (int)wrapAux.Desc.Format, - (int)mapped.RowPitch); + var specs = new RawImageSpecification(desc, mapped.RowPitch); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); } finally { - if (mapWhat is not null) - wrapAux.CtxPtr->Unmap(mapWhat, 0); + context.Get()->Unmap(mapWhat, 0); } } } @@ -226,35 +225,34 @@ internal sealed partial class TextureManager this.device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - await this.interfaceManager.RunBeforeImGuiRender( - () => - { - unsafe - { - using var rtvCopyTemp = default(ComPtr); - var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - tex2DCopyTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - this.device.Get()->CreateRenderTargetView( - (ID3D11Resource*)tex2DCopyTemp.Get(), - &rtvCopyTempDesc, - rtvCopyTemp.GetAddressOf()).ThrowOnError(); - - wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.SimpleDrawer.Draw( - wrapAux.CtxPtr, - wrapAux.SrvPtr, - args.Uv0, - args.Uv1Effective); - if (args.MakeOpaque) - this.SimpleDrawer.StripAlpha(wrapAux.CtxPtr); - - var dummy = default(ID3D11RenderTargetView*); - wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); - } - }); + await this.RunDuringPresent(() => DrawSourceTextureToTarget(wrapAux, args, this.SimpleDrawer, tex2DCopyTemp)); return new(tex2DCopyTemp); + + static unsafe void DrawSourceTextureToTarget( + WrapAux wrapAux, + TextureModificationArgs args, + SimpleDrawerImpl simpleDrawer, + ComPtr tex2DCopyTemp) + { + using var rtvCopyTemp = default(ComPtr); + var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + tex2DCopyTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + wrapAux.DevPtr->CreateRenderTargetView( + (ID3D11Resource*)tex2DCopyTemp.Get(), + &rtvCopyTempDesc, + rtvCopyTemp.GetAddressOf()) + .ThrowOnError(); + + wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + simpleDrawer.Draw(wrapAux.CtxPtr, wrapAux.SrvPtr, args.Uv0, args.Uv1Effective); + if (args.MakeOpaque) + simpleDrawer.StripAlpha(wrapAux.CtxPtr); + + var dummy = default(ID3D11RenderTargetView*); + wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); + } } /// Auxiliary data from . diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 71f300479..9a7d84deb 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -88,25 +88,28 @@ internal sealed partial class TextureManager /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => + public SharedImmediateTexture.PureImpl GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) => - this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture.PureImpl GetFromGame(string path) => + this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) => - this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture.PureImpl GetFromFile(string path) => + this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) => + public SharedImmediateTexture.PureImpl GetFromManifestResource(Assembly assembly, string name) => this.manifestResourceDict.GetOrAdd( (assembly, name), - ManifestResourceSharedImmediateTexture.CreatePlaceholder); + ManifestResourceSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// Invalidates a cached item from and . /// diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index bc10e5b77..40b9d3ee0 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -318,6 +318,7 @@ internal sealed partial class TextureManager { // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT => GUID.GUID_WICPixelFormat96bppRGBFloat, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, @@ -497,10 +498,10 @@ internal sealed partial class TextureManager var outPixelFormat = specs.Format switch { DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() => - GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT => GUID.GUID_WICPixelFormat96bppRGBFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat24bppBGR, DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat8bppGray, @@ -508,9 +509,25 @@ internal sealed partial class TextureManager DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat8bppGray, DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppGray, - _ => GUID.GUID_WICPixelFormat24bppBGR, + _ => GUID.GUID_WICPixelFormat32bppBGRA, }; + var accepted = false; + foreach (var pfi in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICPixelFormat)) + { + Guid tmp; + if (pfi.Get()->GetFormatGUID(&tmp).FAILED) + continue; + accepted = tmp == outPixelFormat; + if (accepted) + break; + } + + if (!accepted) + outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) .ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -613,8 +630,7 @@ internal sealed partial class TextureManager private readonly WICComponentType componentType; /// Initializes a new instance of the struct. - /// The WIC factory. Ownership is not transferred. - /// + /// The WIC factory. Ownership is not transferred. /// The component type to enumerate. public ComponentEnumerable(ComPtr factory, WICComponentType componentType) { diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index f69f01877..7510ec7dc 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -8,6 +8,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -127,7 +128,7 @@ internal sealed partial class TextureManager this.BlameSetName( this.NoThrottleCreateFromImage(bytes.ToArray(), ct), debugName ?? - $"{nameof(this.CreateFromImageAsync)}({nameof(bytes)}, {nameof(cancellationToken)})"), + $"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)"), ct), cancellationToken); @@ -146,7 +147,7 @@ internal sealed partial class TextureManager return this.BlameSetName( this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), debugName ?? - $"{nameof(this.CreateFromImageAsync)}({nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromImageAsync)}(stream)"); }, cancellationToken, leaveOpen ? null : stream); @@ -159,7 +160,7 @@ internal sealed partial class TextureManager string? debugName = null) => this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes), - debugName ?? $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})"); /// public Task CreateFromRawAsync( @@ -173,7 +174,7 @@ internal sealed partial class TextureManager this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes.Span), debugName ?? - $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(bytes)}, {nameof(cancellationToken)})")), + $"{nameof(this.CreateFromRawAsync)}({specs}, {bytes.Length:n0})")), cancellationToken); /// @@ -192,7 +193,7 @@ internal sealed partial class TextureManager return this.BlameSetName( this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), debugName ?? - $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromRawAsync)}({specs}, stream)"); }, cancellationToken, leaveOpen ? null : stream); @@ -207,15 +208,19 @@ internal sealed partial class TextureManager public Task CreateFromTexFileAsync( TexFile file, string? debugName = null, - CancellationToken cancellationToken = default) => - this.DynamicPriorityTextureLoader.LoadAsync( + CancellationToken cancellationToken = default) + { + return this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( this.BlameSetName( this.NoThrottleCreateFromTexFile(file), - debugName ?? $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), + debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), cancellationToken); + static T? ForceNullable(T s) => s; + } + /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); @@ -267,7 +272,7 @@ internal sealed partial class TextureManager .ThrowOnError(); var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})"); return wrap; } @@ -289,8 +294,10 @@ internal sealed partial class TextureManager } var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(file)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); return wrap; + + static T? ForceNullable(T s) => s; } /// Creates a texture from the given , trying to interpret it as a @@ -315,9 +322,31 @@ internal sealed partial class TextureManager // Note: FileInfo and FilePath are not used from TexFile; skip it. var wrap = this.NoThrottleCreateFromTexFile(tf); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(fileBytes)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); return wrap; } private void ReleaseUnmanagedResources() => this.device.Reset(); + + /// Runs the given action in IDXGISwapChain.Present immediately or waiting as needed. + /// The action to run. + // Not sure why this and the below can't be unconditional RunOnFrameworkThread + private async Task RunDuringPresent(Action action) + { + if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + action(); + else + await this.interfaceManager.RunBeforeImGuiRender(action); + } + + /// Runs the given function in IDXGISwapChain.Present immediately or waiting as needed. + /// The type of the return value. + /// The function to run. + /// The return value from the function. + private async Task RunDuringPresent(Func func) + { + if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + return func(); + return await this.interfaceManager.RunBeforeImGuiRender(func); + } } diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index 9ac649812..e2bad7e5f 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -12,7 +12,7 @@ public record struct RawImageSpecification /// The height of the raw image. /// The DXGI format of the raw image. /// The pitch of the raw image in bytes. - /// Specify -1 to calculate it from other parameters. + /// Specify -1 to calculate from other parameters. public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1) { if (pitch < 0) @@ -31,6 +31,14 @@ public record struct RawImageSpecification this.DxgiFormat = dxgiFormat; } + /// Initializes a new instance of the class. + /// The source texture description. + /// The pitch of the raw image in bytes. + internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc, uint pitch) + : this((int)desc.Width, (int)desc.Height, (int)desc.Format, checked((int)pitch)) + { + } + /// Gets or sets the width of the raw image. public int Width { get; set; } @@ -102,6 +110,10 @@ public record struct RawImageSpecification public static RawImageSpecification A8(int width, int height) => new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width); + /// + public override string ToString() => + $"{nameof(RawImageSpecification)}({this.Width}x{this.Height}, {this.Format}, {this.Pitch}b)"; + private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression) { switch (format) @@ -246,7 +258,7 @@ public record struct RawImageSpecification return true; case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: bitsPerPixel = 16; - isBlockCompression = true; + isBlockCompression = false; return true; default: bitsPerPixel = 0; diff --git a/Dalamud/Interface/Textures/TextureModificationArgs.cs b/Dalamud/Interface/Textures/TextureModificationArgs.cs index fac04189c..abccca6b5 100644 --- a/Dalamud/Interface/Textures/TextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/TextureModificationArgs.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Text; using Dalamud.Plugin.Services; @@ -52,6 +53,36 @@ public record struct TextureModificationArgs() /// Gets the effective value of . internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(TextureModificationArgs)).Append('('); + if (this.MakeOpaque) + sb.Append($"{nameof(this.MakeOpaque)}, "); + if (this.Format != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + sb.Append(Enum.GetName(this.Format) is { } name ? name[12..] : this.Format.ToString()).Append(", "); + if (this.NewWidth != 0 || this.NewHeight != 0) + { + sb.Append(this.NewWidth == 0 ? "?" : this.NewWidth.ToString()) + .Append('x') + .Append(this.NewHeight == 0 ? "?" : this.NewHeight.ToString()) + .Append(", "); + } + + if (this.Uv0 != Vector2.Zero || this.Uv1Effective != Vector2.One) + { + sb.Append(this.Uv0.ToString()) + .Append('-') + .Append(this.Uv1.ToString()) + .Append(", "); + } + + if (sb[^1] != '(') + sb.Remove(sb.Length - 2, 2); + return sb.Append(')').ToString(); + } + /// Test if this instance of does not instruct to change the /// underlying data of a texture. /// The texture description to test against. diff --git a/Dalamud/Interface/Textures/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs similarity index 100% rename from Dalamud/Interface/Textures/DalamudTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs diff --git a/Dalamud/Interface/Textures/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Textures/ForwardingTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs index c398fc55c..8b0516e03 100644 --- a/Dalamud/Interface/Textures/ForwardingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs @@ -3,11 +3,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Interface.Internal; -using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Textures; +namespace Dalamud.Interface.Textures.TextureWraps; /// Base class for implementations of that forwards to another. public abstract class ForwardingTextureWrap : IDalamudTextureWrap diff --git a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Textures/IDalamudTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs index c51a69f06..09d64ad21 100644 --- a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs @@ -2,6 +2,8 @@ using System.Numerics; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs similarity index 91% rename from Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs index 88d8d9ca0..0dd5c9f25 100644 --- a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Internal; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that ignores calls. internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap diff --git a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs similarity index 88% rename from Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs index c159f9b36..ec23d7d03 100644 --- a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs @@ -1,11 +1,12 @@ using System.Threading; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Utility; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that is created from an . internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable @@ -50,6 +51,10 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred GC.SuppressFinalize(this); } + /// + public override string ToString() => + $"{nameof(UnknownTextureWrap)}({Service.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})"; + /// Actually dispose the wrapped texture. void IDeferredDisposable.RealDispose() { diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs similarity index 99% rename from Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs index 77ddc2e34..ad3188925 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -15,7 +16,7 @@ using TerraFX.Interop.Windows; using NotSupportedException = System.NotSupportedException; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3b6a754a9..4263fb1bb 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -32,6 +32,7 @@ namespace Dalamud.Interface; /// public sealed class UiBuilder : IDisposable { + private readonly LocalPlugin plugin; private readonly Stopwatch stopwatch; private readonly HitchDetector hitchDetector; private readonly string namespaceName; @@ -42,6 +43,8 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] private readonly TextureManagerPluginScoped scopedTextureProvider; private bool hasErrorWindow = false; @@ -64,6 +67,7 @@ public sealed class UiBuilder : IDisposable this.stopwatch = new Stopwatch(); this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; + this.plugin = plugin; this.interfaceManager.Draw += this.OnDraw; this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); @@ -78,7 +82,10 @@ public sealed class UiBuilder : IDisposable .Add( Service .Get() - .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + .CreateFontAtlas( + namespaceName, + FontAtlasAutoRebuildMode.Disable, + ownerPlugin: plugin)); this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; this.FontAtlas.RebuildRecommend += this.RebuildFonts; } @@ -565,7 +572,8 @@ public sealed class UiBuilder : IDisposable .CreateFontAtlas( this.namespaceName + ":" + (debugName ?? "custom"), autoRebuildMode, - isGlobalScaled)); + isGlobalScaled, + this.plugin)); /// /// Add a notification to the notification queue. diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs index 71d1bf928..1e33cd371 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Dalamud.Utility; using Lumina.Data.Files; diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index a8566d5eb..dfb785c92 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Http; @@ -206,7 +207,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.cancellationTokenSource.Token); } - for (var j = RenameAttemptCount;; j--) + for (var j = RenameAttemptCount; ; j--) { try { @@ -311,17 +312,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA var length = checked((int)stream.Length); buf = ArrayPool.Shared.Rent(length); stream.ReadExactly(buf, 0, length); + var name = $"{nameof(DalamudAsset)}[{Enum.GetName(asset)}]"; var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync( - buf, - $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}"), + DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf, name), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? await tm.CreateFromRawAsync( - raw.Specification, - buf, - $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}") + ? await tm.CreateFromRawAsync(raw.Specification, buf, name) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null,