This commit is contained in:
Soreepeong 2024-03-05 17:41:27 +09:00
parent 9ba0a297c9
commit 70eecdaaef
30 changed files with 767 additions and 345 deletions

View file

@ -1,20 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Interface;
/// <summary>
/// Represents a lookup for a game icon.
/// </summary>
/// <param name="IconId">The icon ID.</param>
/// <param name="ItemHq">Whether the HQ icon is requested, where HQ is in the context of items.</param>
/// <param name="HiRes">Whether the high-resolution icon is requested.</param>
/// <param name="Language">The language of the icon to load.</param>
[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);

View file

@ -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;
/// <summary>
@ -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();
}
/// <summary>Shows a context menu confirming texture save.</summary>
/// <param name="initiatorName">Name of the initiator.</param>
/// <param name="name">Suggested name of the file being saved.</param>
/// <param name="texture">A task returning the texture to save.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ShowTextureSaveMenuAsync(
string initiatorName,
string name,
Task<IDalamudTextureWrap> texture)
{
try
{
var initiatorScreenOffset = ImGui.GetMousePos();
using var textureWrap = await texture;
var textureManager = await Service<TextureManager>.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}";
BitmapCodecInfo encoder;
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo>();
Service<InterfaceManager>.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<InterfaceManager>.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<string>();
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<string, object>();
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<NotificationManager>.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<NotificationManager>.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)
{

View file

@ -199,6 +199,10 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public bool IsDispatchingEvents { get; set; } = true;
/// <summary>Gets a value indicating whether the main thread is executing <see cref="PresentDetour"/>.</summary>
/// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks>
public bool IsInPresent { get; private set; }
/// <summary>
/// Gets a value indicating the native handle of the game main window.
/// </summary>
@ -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);
}

View file

@ -142,7 +142,7 @@ public class IconBrowserWidget : IDataWindowWidget
var texm = Service<TextureManager>.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<DalamudInterface>.Get().ShowTextureSaveMenuAsync(
this.DisplayName,
iconId.ToString(),
Task.FromResult(texture.CreateWrapSharingLowLevelResource()));
}
ImGui.GetWindowDrawList().AddRect(
cursor,
cursor + this.iconSize,

View file

@ -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;
/// </summary>
internal class TexWidget : IDataWindowWidget
{
private static readonly Dictionary<
DrawBlameTableColumnUserId,
Func<TextureManager.IBlameableDalamudTextureWrap, IComparable>> 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<TextureEntry> 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,
}
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "tex", "texture" };
@ -74,6 +95,7 @@ internal class TexWidget : IDataWindowWidget
/// <inheritdoc/>
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<TextureManager>.Get();
var conf = Service<DalamudConfiguration>.Get();
if (ImGui.Button("GC"))
GC.Collect();
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");
if (ImGui.CollapsingHeader($"All Loaded Textures: {this.textureManager.AllBlamesForDebug.Count:g}###header"))
this.DrawBlame(this.textureManager.AllBlamesForDebug);
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<DalamudInterface>.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<TextureManager.IBlameableDalamudTextureWrap> allBlames)
private unsafe void DrawBlame(List<TextureManager.IBlameableDalamudTextureWrap> allBlames)
{
var conf = Service<DalamudConfiguration>.Get();
var im = Service<InterfaceManager>.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<ImGuiTableColumnSortSpecs>(sortSpecs.NativePtr->Specs, sortSpecs.SpecsCount);
Span<bool> 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<DalamudInterface>.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<DalamudInterface>.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<Task<IDalamudTextureWrap>> textureGetter)
{
try
{
BitmapCodecInfo encoder;
{
var off = ImGui.GetCursorScreenPos();
var first = true;
var encoders = this.textureManager
.Wic
.GetSupportedEncoderInfos()
.ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo>();
Service<InterfaceManager>.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<InterfaceManager>.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<string>();
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<string, object>();
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<NotificationManager>.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<NotificationManager>.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();
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);

View file

@ -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;

View file

@ -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
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas are under the effect of global scale.</param>
/// <param name="ownerPlugin">The owner plugin, if any.</param>
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
/// <inheritdoc/>
public bool IsGlobalScaled { get; }
/// <summary>Gets the owner plugin, if any.</summary>
public LocalPlugin? OwnerPlugin { get; }
/// <inheritdoc/>
public void Dispose()
{

View file

@ -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
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <param name="ownerPlugin">The owner plugin, if any.</param>
/// <returns>The new font atlas.</returns>
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);
/// <summary>
/// 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
{

View file

@ -0,0 +1,55 @@
using System.Text;
namespace Dalamud.Interface.Textures;
/// <summary>Represents a lookup for a game icon.</summary>
public readonly record struct GameIconLookup
{
/// <summary>Initializes a new instance of the <see cref="GameIconLookup"/> class.</summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="itemHq">Whether the HQ icon is requested, where HQ is in the context of items.</param>
/// <param name="hiRes">Whether the high-resolution icon is requested.</param>
/// <param name="language">The language of the icon to load.</param>
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);
/// <summary>Gets the icon ID.</summary>
public uint IconId { get; init; }
/// <summary>Gets a value indicating whether the HQ icon is requested, where HQ is in the context of items.</summary>
public bool ItemHq { get; init; }
/// <summary>Gets a value indicating whether the high-resolution icon is requested.</summary>
public bool HiRes { get; init; }
/// <summary>Gets the language of the icon to load.</summary>
/// <remarks>
/// <para><c>null</c> will use the active game language.</para>
/// <para>If the specified resource does not have variants per language, the language-neutral texture will be used.
/// </para>
/// </remarks>
public ClientLanguage? Language { get; init; }
/// <inheritdoc/>
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();
}
}

View file

@ -1,9 +1,12 @@
using System.Numerics;
using System.Text;
using Dalamud.Interface.Internal;
using ImGuiNET;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures;
/// <summary>Describes how to take a texture of an existing ImGui viewport.</summary>
@ -44,6 +47,30 @@ public record struct ImGuiViewportTextureArgs()
/// <summary>Gets the effective value of <see cref="Uv1"/>.</summary>
internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1;
/// <inheritdoc/>
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();
}
/// <summary>Checks the properties and throws an exception if values are invalid.</summary>
internal void ThrowOnInvalidValues()
{

View file

@ -18,6 +18,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture
/// <summary>Creates a new placeholder instance of <see cref="GamePathSharedImmediateTexture"/>.</summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromFile"/>.</remarks>
public static SharedImmediateTexture CreatePlaceholder(string path) => new FileSystemSharedImmediateTexture(path);
/// <inheritdoc/>

View file

@ -22,6 +22,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
/// <summary>Creates a new placeholder instance of <see cref="GamePathSharedImmediateTexture"/>.</summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromGame"/>.</remarks>
public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path);
/// <inheritdoc/>

View file

@ -27,6 +27,8 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe
/// <summary>Creates a new placeholder instance of <see cref="ManifestResourceSharedImmediateTexture"/>.</summary>
/// <param name="args">The arguments to pass to the constructor.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromManifestResource"/>.
/// </remarks>
public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) =>
new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name);

View file

@ -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<LocalPlugin> 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);
}
/// <summary>Gets the list of owner plugins.</summary>
public List<LocalPlugin> OwnerPlugins { get; } = new();
/// <summary>Gets a wrapper for this instance which disables resource reference management.</summary>
public PureImpl PublicUseInstance { get; }
/// <summary>Gets the instance ID. Debug use only.</summary>
public long InstanceIdForDebug { get; }
@ -280,15 +284,15 @@ internal abstract class SharedImmediateTexture
return this.availableOnAccessWrapForApi9;
}
/// <summary>Adds a plugin to <see cref="OwnerPlugins"/>, in a thread-safe way.</summary>
/// <summary>Adds a plugin to <see cref="ownerPlugins"/>, in a thread-safe way.</summary>
/// <param name="plugin">The plugin to add.</param>
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<TextureManager>.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))
lock (this.ownerPlugins)
{
foreach (var op in this.ownerPlugins.Take(addLen))
Service<TextureManager>.Get().Blame(r.Result, op);
}
},
default(CancellationToken));
}
@ -427,6 +434,52 @@ internal abstract class SharedImmediateTexture
}
}
/// <summary>A wrapper around <see cref="SharedImmediateTexture"/>, to prevent external consumers from mistakenly
/// calling <see cref="IDisposable.Dispose"/> or <see cref="IRefCountable.Release"/>.</summary>
internal sealed class PureImpl : ISharedImmediateTexture
{
private readonly SharedImmediateTexture inner;
/// <summary>Initializes a new instance of the <see cref="PureImpl"/> class.</summary>
/// <param name="inner">The actual instance.</param>
public PureImpl(SharedImmediateTexture inner) => this.inner = inner;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap GetWrapOrEmpty() =>
this.inner.GetWrapOrEmpty();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[return: NotNullIfNotNull(nameof(defaultWrap))]
public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null) =>
this.inner.GetWrapOrDefault(defaultWrap);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) =>
this.inner.TryGetWrap(out texture, out exception);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Task<IDalamudTextureWrap> RentAsync(CancellationToken cancellationToken = default) =>
this.inner.RentAsync(cancellationToken);
/// <inheritdoc cref="SharedImmediateTexture.GetAvailableOnAccessWrapForApi9"/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap? GetAvailableOnAccessWrapForApi9() =>
this.inner.GetAvailableOnAccessWrapForApi9();
/// <inheritdoc cref="SharedImmediateTexture.AddOwnerPlugin"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddOwnerPlugin(LocalPlugin plugin) =>
this.inner.AddOwnerPlugin(plugin);
/// <inheritdoc/>
public override string ToString() => $"{this.inner}({nameof(PureImpl)})";
}
/// <summary>Same with <see cref="DisposeSuppressingTextureWrap"/>, but with a custom implementation of
/// <see cref="CreateWrapSharingLowLevelResource"/>.</summary>
private sealed class NotOwnedTextureWrap : DisposeSuppressingTextureWrap

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
private readonly List<BlameTag> blameTracker = new();
/// <summary>A wrapper for underlying texture2D resources.</summary>
public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap
{
/// <summary>Gets the address of the native resource.</summary>
public nint ResourceAddress { get; }
/// <summary>Gets the name of the underlying resource of this texture wrap.</summary>
public string Name { get; }
@ -31,13 +31,27 @@ internal sealed partial class TextureManager
/// <summary>Gets the list of owner plugins.</summary>
public List<LocalPlugin> OwnerPlugins { get; }
/// <summary>Gets the raw image specification.</summary>
public RawImageSpecification RawSpecs { get; }
/// <summary>Tests whether the tag and the underlying resource are released or should be released.</summary>
/// <returns><c>true</c> if there are no more remaining references to this instance.</returns>
bool TestIsReleasedOrShouldRelease();
}
/// <summary>Gets all the loaded textures from plugins.</summary>
/// <returns>The enumerable that goes through all textures and relevant plugins.</returns>
/// <summary>Gets the list containing all the loaded textures from plugins.</summary>
/// <remarks>Returned value must be used inside a lock.</remarks>
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Caller locks the return value.")]
public IReadOnlyList<IBlameableDalamudTextureWrap> AllBlamesForDebug => this.blameTracker;
public List<IBlameableDalamudTextureWrap> BlameTracker { get; } = new();
/// <summary>Gets the blame for a texture wrap.</summary>
/// <param name="textureWrap">The texture wrap.</param>
/// <returns>The blame, if it exists.</returns>
public unsafe IBlameableDalamudTextureWrap? GetBlame(IDalamudTextureWrap textureWrap)
{
using var wrapAux = new WrapAux(textureWrap, true);
return BlameTag.Get(wrapAux.ResPtr);
}
/// <summary>Puts a plugin on blame for a texture.</summary>
/// <param name="textureWrap">The texture.</param>
@ -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
/// <inheritdoc/>
public List<LocalPlugin> OwnerPlugins { get; } = new();
/// <inheritdoc/>
public nint ResourceAddress => (nint)this.tex2D;
/// <inheritdoc/>
public string Name { get; set; } = "<unnamed>";
/// <inheritdoc/>
public DXGI_FORMAT Format => this.desc.Format;
/// <inheritdoc/>
public RawImageSpecification RawSpecs => new(
(int)this.desc.Width,
(int)this.desc.Height,
(int)this.desc.Format,
0);
/// <inheritdoc/>
public IntPtr ImGuiHandle
{
@ -267,7 +291,23 @@ internal sealed partial class TextureManager
/// <param name="isNew"><c>true</c> if the tracker is new.</param>
/// <typeparam name="T">A COM object type.</typeparam>
/// <returns>A new instance of <see cref="BlameTag"/>.</returns>
public static BlameTag From<T>(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface
public static BlameTag GetOrCreate<T>(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);
}
/// <summary>Gets an existing instance of <see cref="BlameTag"/> for the given resource.</summary>
/// <param name="trackWhat">The COM object to track.</param>
/// <typeparam name="T">A COM object type.</typeparam>
/// <returns>An existing instance of <see cref="BlameTag"/>.</returns>
public static BlameTag? Get<T>(T* trackWhat) where T : unmanaged, IUnknown.Interface
{
using var deviceChild = default(ComPtr<ID3D11DeviceChild>);
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;
}
/// <summary>Tests whether the tag and the underlying resource are released or should be released.</summary>
/// <returns><c>true</c> if there are no more remaining references to this instance.</returns>
/// <inheritdoc/>
public bool TestIsReleasedOrShouldRelease()
{
if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64)

View file

@ -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,23 +137,28 @@ 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<ID3D11Texture2D> tex2D,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ID3D11Resource* mapWhat = null;
try
{
D3D11_TEXTURE2D_DESC desc;
tex2D.Get()->GetDesc(&desc);
using var device = default(ComPtr<ID3D11Device>);
tex2D.Get()->GetDevice(device.GetAddressOf());
using var context = default(ComPtr<ID3D11DeviceContext>);
device.Get()->GetImmediateContext(context.GetAddressOf());
using var tmpTex = default(ComPtr<ID3D11Texture2D>);
if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0)
if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0)
{
var tmpTexDesc = wrapAux.Desc with
var tmpTexDesc = desc with
{
MipLevels = 1,
ArraySize = 1,
@ -162,33 +168,26 @@ internal sealed partial class TextureManager
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());
device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError();
context.Get()->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 mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get());
var specs = new RawImageSpecification(
(int)wrapAux.Desc.Width,
(int)wrapAux.Desc.Height,
(int)wrapAux.Desc.Format,
(int)mapped.RowPitch);
D3D11_MAPPED_SUBRESOURCE mapped;
context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError();
try
{
var specs = new RawImageSpecification(desc, mapped.RowPitch);
var bytes = new Span<byte>(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
await this.RunDuringPresent(() => DrawSourceTextureToTarget(wrapAux, args, this.SimpleDrawer, tex2DCopyTemp));
return new(tex2DCopyTemp);
static unsafe void DrawSourceTextureToTarget(
WrapAux wrapAux,
TextureModificationArgs args,
SimpleDrawerImpl simpleDrawer,
ComPtr<ID3D11Texture2D> tex2DCopyTemp)
{
using var rtvCopyTemp = default(ComPtr<ID3D11RenderTargetView>);
var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC(
tex2DCopyTemp,
D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D);
this.device.Get()->CreateRenderTargetView(
wrapAux.DevPtr->CreateRenderTargetView(
(ID3D11Resource*)tex2DCopyTemp.Get(),
&rtvCopyTempDesc,
rtvCopyTemp.GetAddressOf()).ThrowOnError();
rtvCopyTemp.GetAddressOf())
.ThrowOnError();
wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null);
this.SimpleDrawer.Draw(
wrapAux.CtxPtr,
wrapAux.SrvPtr,
args.Uv0,
args.Uv1Effective);
simpleDrawer.Draw(wrapAux.CtxPtr, wrapAux.SrvPtr, args.Uv0, args.Uv1Effective);
if (args.MakeOpaque)
this.SimpleDrawer.StripAlpha(wrapAux.CtxPtr);
simpleDrawer.StripAlpha(wrapAux.CtxPtr);
var dummy = default(ID3D11RenderTargetView*);
wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null);
}
});
return new(tex2DCopyTemp);
}
/// <summary>Auxiliary data from <see cref="IDalamudTextureWrap"/>.</summary>

View file

@ -88,25 +88,28 @@ internal sealed partial class TextureManager
/// <inheritdoc cref="ITextureProvider.GetFromGameIcon"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) =>
public SharedImmediateTexture.PureImpl GetFromGameIcon(in GameIconLookup lookup) =>
this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc cref="ITextureProvider.GetFromGame"/>
[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;
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[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;
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[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;
/// <summary>Invalidates a cached item from <see cref="GetFromGame"/> and <see cref="GetFromGameIcon"/>.
/// </summary>

View file

@ -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<IWICPixelFormatInfo>(
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;
/// <summary>Initializes a new instance of the <see cref="ComponentEnumerable{T}"/> struct.</summary>
/// <param name="factory">The WIC factory. Ownership is not transferred.
/// </param>
/// <param name="factory">The WIC factory. Ownership is not transferred.</param>
/// <param name="componentType">The component type to enumerate.</param>
public ComponentEnumerable(ComPtr<IWICImagingFactory> factory, WICComponentType componentType)
{

View file

@ -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})");
/// <inheritdoc/>
public Task<IDalamudTextureWrap> 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);
/// <inheritdoc/>
@ -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<IDalamudTextureWrap> 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>(T s) => s;
}
/// <inheritdoc/>
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>(T s) => s;
}
/// <summary>Creates a texture from the given <paramref name="fileBytes"/>, 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();
/// <summary>Runs the given action in IDXGISwapChain.Present immediately or waiting as needed.</summary>
/// <param name="action">The action to run.</param>
// 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);
}
/// <summary>Runs the given function in IDXGISwapChain.Present immediately or waiting as needed.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function to run.</param>
/// <returns>The return value from the function.</returns>
private async Task<T> RunDuringPresent<T>(Func<T> func)
{
if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread)
return func();
return await this.interfaceManager.RunBeforeImGuiRender(func);
}
}

View file

@ -12,7 +12,7 @@ public record struct RawImageSpecification
/// <param name="height">The height of the raw image.</param>
/// <param name="dxgiFormat">The DXGI format of the raw image.</param>
/// <param name="pitch">The pitch of the raw image in bytes.
/// Specify <c>-1</c> to calculate it from other parameters.</param>
/// Specify <c>-1</c> to calculate from other parameters.</param>
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;
}
/// <summary>Initializes a new instance of the <see cref="RawImageSpecification"/> class.</summary>
/// <param name="desc">The source texture description.</param>
/// <param name="pitch">The pitch of the raw image in bytes.</param>
internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc, uint pitch)
: this((int)desc.Width, (int)desc.Height, (int)desc.Format, checked((int)pitch))
{
}
/// <summary>Gets or sets the width of the raw image.</summary>
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);
/// <inheritdoc/>
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;

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System.Text;
using Dalamud.Plugin.Services;
@ -52,6 +53,36 @@ public record struct TextureModificationArgs()
/// <summary>Gets the effective value of <see cref="Uv1"/>.</summary>
internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1;
/// <inheritdoc/>
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();
}
/// <summary>Test if this instance of <see cref="TextureModificationArgs"/> does not instruct to change the
/// underlying data of a texture.</summary>
/// <param name="desc">The texture description to test against.</param>

View file

@ -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;
/// <summary>Base class for implementations of <see cref="IDalamudTextureWrap"/> that forwards to another.</summary>
public abstract class ForwardingTextureWrap : IDalamudTextureWrap

View file

@ -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;

View file

@ -1,6 +1,6 @@
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal;
namespace Dalamud.Interface.Textures.TextureWraps.Internal;
/// <summary>A texture wrap that ignores <see cref="IDisposable.Dispose"/> calls.</summary>
internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap

View file

@ -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;
/// <summary>A texture wrap that is created from an <see cref="IUnknown"/>.</summary>
internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable
@ -50,6 +51,10 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(UnknownTextureWrap)}({Service<TextureManager>.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})";
/// <summary>Actually dispose the wrapped texture.</summary>
void IDeferredDisposable.RealDispose()
{

View file

@ -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;
/// <summary>A texture wrap that takes its buffer from the frame buffer (of swap chain).</summary>
internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable

View file

@ -32,6 +32,7 @@ namespace Dalamud.Interface;
/// </summary>
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<DalamudConfiguration>.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<FontAtlasFactory>
.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));
/// <summary>
/// Add a notification to the notification queue.

View file

@ -2,6 +2,7 @@
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Dalamud.Utility;
using Lumina.Data.Files;

View file

@ -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;
@ -311,17 +312,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
var length = checked((int)stream.Length);
buf = ArrayPool<byte>.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<DalamudAssetRawTextureAttribute>() 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,