diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index a0322aab13..c9ba20f411 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -121,6 +121,13 @@
+
+
+
+
+
+
+
diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs
new file mode 100644
index 0000000000..87e0032882
--- /dev/null
+++ b/Dalamud/Interface/Internal/StaThreadService.cs
@@ -0,0 +1,282 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Utility;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.Internal;
+
+/// Dedicated thread for OLE operations, and possibly more native thread-serialized operations.
+[ServiceManager.EarlyLoadedService]
+internal partial class StaThreadService : IInternalDisposableService
+{
+ private readonly CancellationTokenSource cancellationTokenSource = new();
+ private readonly Thread thread;
+ private readonly ThreadBoundTaskScheduler taskScheduler;
+ private readonly TaskFactory taskFactory;
+
+ private readonly TaskCompletionSource messageReceiverHwndTask =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ [ServiceManager.ServiceConstructor]
+ private StaThreadService()
+ {
+ try
+ {
+ this.thread = new(this.OleThreadBody);
+ this.thread.SetApartmentState(ApartmentState.STA);
+
+ this.taskScheduler = new(this.thread);
+ this.taskScheduler.TaskQueued += this.TaskSchedulerOnTaskQueued;
+ this.taskFactory = new(
+ this.cancellationTokenSource.Token,
+ TaskCreationOptions.None,
+ TaskContinuationOptions.None,
+ this.taskScheduler);
+
+ this.thread.Start();
+ this.messageReceiverHwndTask.Task.Wait();
+ }
+ catch (Exception e)
+ {
+ this.cancellationTokenSource.Cancel();
+ this.messageReceiverHwndTask.SetException(e);
+ throw;
+ }
+ }
+
+ /// Gets all the available clipboard formats.
+ public IReadOnlySet AvailableClipboardFormats { get; private set; } = ImmutableSortedSet.Empty;
+
+ /// Places a pointer to a specific data object onto the clipboard. This makes the data object accessible
+ /// to the function.
+ /// Pointer to the interface on the data object from which the data to
+ /// be placed on the clipboard can be obtained. This parameter can be NULL; in which case the clipboard is emptied.
+ ///
+ /// This function returns on success.
+ [LibraryImport("ole32.dll")]
+ public static unsafe partial int OleSetClipboard(IDataObject* pdo);
+
+ ///
+ public static unsafe void OleSetClipboard(ComPtr pdo) =>
+ Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get()));
+
+ /// Retrieves a data object that you can use to access the contents of the clipboard.
+ /// Address of pointer variable that receives the interface pointer to
+ /// the clipboard data object.
+ /// This function returns on success.
+ [LibraryImport("ole32.dll")]
+ public static unsafe partial int OleGetClipboard(IDataObject** pdo);
+
+ ///
+ public static unsafe ComPtr OleGetClipboard()
+ {
+ var pdo = default(ComPtr);
+ Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf()));
+ return pdo;
+ }
+
+ /// Calls the appropriate method or function to release the specified storage medium.
+ /// Address of to release.
+ [LibraryImport("ole32.dll")]
+ public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm);
+
+ ///
+ public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm)
+ {
+ fixed (STGMEDIUM* pstgm = &stgm)
+ ReleaseStgMedium(pstgm);
+ }
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.cancellationTokenSource.Cancel();
+ if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully)
+ SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0);
+
+ this.thread.Join();
+ }
+
+ /// Runs a given delegate in the messaging thread.
+ /// Delegate to run.
+ /// Optional cancellation token.
+ /// A representating the state of the operation.
+ public async Task Run(Action action, CancellationToken cancellationToken = default)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(
+ this.cancellationTokenSource.Token,
+ cancellationToken);
+ await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true);
+ }
+
+ /// Runs a given delegate in the messaging thread.
+ /// Type of the return value.
+ /// Delegate to run.
+ /// Optional cancellation token.
+ /// A representating the state of the operation.
+ public async Task Run(Func func, CancellationToken cancellationToken = default)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(
+ this.cancellationTokenSource.Token,
+ cancellationToken);
+ return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
+ }
+
+ /// Runs a given delegate in the messaging thread.
+ /// Delegate to run.
+ /// Optional cancellation token.
+ /// A representating the state of the operation.
+ public async Task Run(Func func, CancellationToken cancellationToken = default)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(
+ this.cancellationTokenSource.Token,
+ cancellationToken);
+ await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
+ }
+
+ /// Runs a given delegate in the messaging thread.
+ /// Type of the return value.
+ /// Delegate to run.
+ /// Optional cancellation token.
+ /// A representating the state of the operation.
+ public async Task Run(Func> func, CancellationToken cancellationToken = default)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(
+ this.cancellationTokenSource.Token,
+ cancellationToken);
+ return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
+ }
+
+ [LibraryImport("ole32.dll")]
+ private static partial int OleInitialize(nint reserved);
+
+ [LibraryImport("ole32.dll")]
+ private static partial void OleUninitialize();
+
+ [LibraryImport("ole32.dll")]
+ private static partial int OleFlushClipboard();
+
+ private void TaskSchedulerOnTaskQueued() =>
+ PostMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_NULL, 0, 0);
+
+ private void UpdateAvailableClipboardFormats(HWND hWnd)
+ {
+ if (!OpenClipboard(hWnd))
+ {
+ this.AvailableClipboardFormats = ImmutableSortedSet.Empty;
+ return;
+ }
+
+ var formats = new SortedSet();
+ for (var cf = EnumClipboardFormats(0); cf != 0; cf = EnumClipboardFormats(cf))
+ formats.Add(cf);
+ this.AvailableClipboardFormats = formats;
+ CloseClipboard();
+ }
+
+ private LRESULT MessageReceiverWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
+ {
+ this.taskScheduler.Run();
+
+ switch (uMsg)
+ {
+ case WM.WM_CLIPBOARDUPDATE:
+ this.UpdateAvailableClipboardFormats(hWnd);
+ break;
+
+ case WM.WM_DESTROY:
+ PostQuitMessage(0);
+ return 0;
+ }
+
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+ }
+
+ private unsafe void OleThreadBody()
+ {
+ var hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(StaThreadService).Module);
+ ushort wndClassAtom = 0;
+ var gch = GCHandle.Alloc(this);
+ try
+ {
+ ((HRESULT)OleInitialize(0)).ThrowOnError();
+
+ fixed (char* name = typeof(StaThreadService).FullName!)
+ {
+ var wndClass = new WNDCLASSEXW
+ {
+ cbSize = (uint)sizeof(WNDCLASSEXW),
+ lpfnWndProc = &MessageReceiverWndProcStatic,
+ hInstance = hInstance,
+ hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1),
+ lpszClassName = (ushort*)name,
+ };
+
+ wndClassAtom = RegisterClassExW(&wndClass);
+ if (wndClassAtom == 0)
+ Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
+
+ this.messageReceiverHwndTask.SetResult(
+ CreateWindowExW(
+ 0,
+ (ushort*)wndClassAtom,
+ (ushort*)name,
+ 0,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ default,
+ default,
+ hInstance,
+ (void*)GCHandle.ToIntPtr(gch)));
+
+ [UnmanagedCallersOnly]
+ static LRESULT MessageReceiverWndProcStatic(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
+ {
+ nint gchn;
+ if (uMsg == WM.WM_NCCREATE)
+ {
+ gchn = (nint)((CREATESTRUCTW*)lParam)->lpCreateParams;
+ SetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA, gchn);
+ }
+ else
+ {
+ gchn = GetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA);
+ }
+
+ if (gchn == 0)
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+
+ return ((StaThreadService)GCHandle.FromIntPtr(gchn).Target!)
+ .MessageReceiverWndProc(hWnd, uMsg, wParam, lParam);
+ }
+ }
+
+ AddClipboardFormatListener(this.messageReceiverHwndTask.Task.Result);
+ this.UpdateAvailableClipboardFormats(this.messageReceiverHwndTask.Task.Result);
+
+ for (MSG msg; GetMessageW(&msg, default, 0, 0);)
+ {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+ }
+ catch (Exception e)
+ {
+ gch.Free();
+ _ = OleFlushClipboard();
+ OleUninitialize();
+ if (wndClassAtom != 0)
+ UnregisterClassW((ushort*)wndClassAtom, hInstance);
+ this.messageReceiverHwndTask.TrySetException(e);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
index 07b2d01ff6..ac48668fb4 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
@@ -182,6 +182,15 @@ public void Draw()
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
+ if (!this.textureManager.HasClipboardImage())
+ {
+ ImGuiComponents.DisabledButton("Paste from Clipboard");
+ }
+ else if (ImGui.Button("Paste from Clipboard"))
+ {
+ this.addedTextures.Add(new(Api10: this.textureManager.CreateFromClipboardAsync()));
+ }
+
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon)))
{
ImGui.PushID(nameof(this.DrawGetFromGameIcon));
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs
new file mode 100644
index 0000000000..8a510e9670
--- /dev/null
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs
@@ -0,0 +1,498 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Interface.Internal;
+using Dalamud.Interface.Textures.TextureWraps;
+using Dalamud.Memory;
+using Dalamud.Utility;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.Textures.Internal;
+
+/// Service responsible for loading and disposing ImGui texture wraps.
+internal sealed partial class TextureManager
+{
+ ///
+ public async Task CopyToClipboardAsync(
+ IDalamudTextureWrap wrap,
+ string? preferredFileNameWithoutExtension = null,
+ bool leaveWrapOpen = false,
+ CancellationToken cancellationToken = default)
+ {
+ ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
+
+ using var wrapAux = new WrapAux(wrap, leaveWrapOpen);
+ bool hasAlphaChannel;
+ switch (wrapAux.Desc.Format)
+ {
+ case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
+ case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
+ hasAlphaChannel = false;
+ break;
+ case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
+ case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
+ case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
+ case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
+ hasAlphaChannel = true;
+ break;
+ default:
+ await this.CopyToClipboardAsync(
+ await this.CreateFromExistingTextureAsync(
+ wrap,
+ new() { Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM },
+ cancellationToken: cancellationToken),
+ preferredFileNameWithoutExtension,
+ false,
+ cancellationToken);
+ return;
+ }
+
+ // https://stackoverflow.com/questions/15689541/win32-clipboard-and-alpha-channel-images
+ // https://learn.microsoft.com/en-us/windows/win32/shell/clipboard
+ using var pdo = default(ComPtr);
+ unsafe
+ {
+ fixed (Guid* piid = &IID.IID_IDataObject)
+ SHCreateDataObject(null, 1, null, null, piid, (void**)pdo.GetAddressOf()).ThrowOnError();
+ }
+
+ var ms = new MemoryStream();
+ {
+ ms.SetLength(ms.Position = 0);
+ await this.SaveToStreamAsync(
+ wrap,
+ GUID.GUID_ContainerFormatPng,
+ ms,
+ new Dictionary { ["InterlaceOption"] = true },
+ true,
+ true,
+ cancellationToken);
+
+ unsafe
+ {
+ using var ims = default(ComPtr);
+ fixed (byte* p = ms.GetBuffer())
+ ims.Attach(SHCreateMemStream(p, (uint)ms.Length));
+ if (ims.IsEmpty())
+ throw new OutOfMemoryException();
+
+ AddToDataObject(
+ pdo,
+ ClipboardFormats.Png,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_ISTREAM,
+ pstm = ims.Get(),
+ });
+ AddToDataObject(
+ pdo,
+ ClipboardFormats.FileContents,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_ISTREAM,
+ pstm = ims.Get(),
+ });
+ ims.Get()->AddRef();
+ ims.Detach();
+ }
+
+ if (preferredFileNameWithoutExtension is not null)
+ {
+ unsafe
+ {
+ preferredFileNameWithoutExtension += ".png";
+ if (preferredFileNameWithoutExtension.Length >= 260)
+ preferredFileNameWithoutExtension = preferredFileNameWithoutExtension[..^4] + ".png";
+ var namea = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8)
+ .GetBytes(preferredFileNameWithoutExtension);
+ if (namea.Length > 260)
+ {
+ namea.AsSpan()[^4..].CopyTo(namea.AsSpan(256, 4));
+ Array.Resize(ref namea, 260);
+ }
+
+ var fgda = new FILEGROUPDESCRIPTORA
+ {
+ cItems = 1,
+ fgd = new()
+ {
+ e0 = new()
+ {
+ dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE),
+ nFileSizeHigh = (uint)(ms.Length >> 32),
+ nFileSizeLow = (uint)ms.Length,
+ },
+ },
+ };
+ namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260));
+
+ AddToDataObject(
+ pdo,
+ ClipboardFormats.FileDescriptorA,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_HGLOBAL,
+ hGlobal = CreateHGlobalFromMemory(new(ref fgda)),
+ });
+
+ var fgdw = new FILEGROUPDESCRIPTORW
+ {
+ cItems = 1,
+ fgd = new()
+ {
+ e0 = new()
+ {
+ dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE),
+ nFileSizeHigh = (uint)(ms.Length >> 32),
+ nFileSizeLow = (uint)ms.Length,
+ },
+ },
+ };
+ preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260));
+
+ AddToDataObject(
+ pdo,
+ ClipboardFormats.FileDescriptorW,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_HGLOBAL,
+ hGlobal = CreateHGlobalFromMemory(new(ref fgdw)),
+ });
+ }
+ }
+ }
+
+ {
+ ms.SetLength(ms.Position = 0);
+ await this.SaveToStreamAsync(
+ wrap,
+ GUID.GUID_ContainerFormatBmp,
+ ms,
+ new Dictionary { ["EnableV5Header32bppBGRA"] = false },
+ true,
+ true,
+ cancellationToken);
+ AddToDataObject(
+ pdo,
+ CF.CF_DIB,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_HGLOBAL,
+ hGlobal = CreateHGlobalFromMemory(
+ ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]),
+ });
+ }
+
+ if (hasAlphaChannel)
+ {
+ ms.SetLength(ms.Position = 0);
+ await this.SaveToStreamAsync(
+ wrap,
+ GUID.GUID_ContainerFormatBmp,
+ ms,
+ new Dictionary { ["EnableV5Header32bppBGRA"] = true },
+ true,
+ true,
+ cancellationToken);
+ AddToDataObject(
+ pdo,
+ CF.CF_DIBV5,
+ new()
+ {
+ tymed = (uint)TYMED.TYMED_HGLOBAL,
+ hGlobal = CreateHGlobalFromMemory(
+ ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]),
+ });
+ }
+
+ var omts = await Service.GetAsync();
+ await omts.Run(() => StaThreadService.OleSetClipboard(pdo), cancellationToken);
+
+ return;
+
+ static unsafe void AddToDataObject(ComPtr pdo, uint clipboardFormat, STGMEDIUM stg)
+ {
+ var fec = new FORMATETC
+ {
+ cfFormat = (ushort)clipboardFormat,
+ ptd = null,
+ dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
+ lindex = 0,
+ tymed = stg.tymed,
+ };
+ pdo.Get()->SetData(&fec, &stg, true).ThrowOnError();
+ }
+
+ static unsafe HGLOBAL CreateHGlobalFromMemory(ReadOnlySpan data) where T : unmanaged
+ {
+ var h = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)(data.Length * sizeof(T)));
+ if (h == 0)
+ throw new OutOfMemoryException("Failed to allocate.");
+
+ var p = GlobalLock(h);
+ data.CopyTo(new(p, data.Length));
+ GlobalUnlock(h);
+ return h;
+ }
+ }
+
+ ///
+ public bool HasClipboardImage()
+ {
+ var acf = Service.Get().AvailableClipboardFormats;
+ return acf.Contains(CF.CF_DIBV5)
+ || acf.Contains(CF.CF_DIB)
+ || acf.Contains(ClipboardFormats.Png)
+ || acf.Contains(ClipboardFormats.FileContents);
+ }
+
+ ///
+ public async Task CreateFromClipboardAsync(
+ string? debugName = null,
+ CancellationToken cancellationToken = default)
+ {
+ var omts = await Service.GetAsync();
+ var (stgm, clipboardFormat) = await omts.Run(GetSupportedClipboardData, cancellationToken);
+
+ try
+ {
+ return this.BlameSetName(
+ await this.DynamicPriorityTextureLoader.LoadAsync(
+ null,
+ ct =>
+ clipboardFormat is CF.CF_DIB or CF.CF_DIBV5
+ ? CreateTextureFromStorageMediumDib(this, stgm, ct)
+ : CreateTextureFromStorageMedium(this, stgm, ct),
+ cancellationToken),
+ debugName ?? $"{nameof(this.CreateFromClipboardAsync)}({(TYMED)stgm.tymed})");
+ }
+ finally
+ {
+ StaThreadService.ReleaseStgMedium(ref stgm);
+ }
+
+ // Converts a CF_DIB/V5 format to a full BMP format, for WIC consumption.
+ static unsafe Task CreateTextureFromStorageMediumDib(
+ TextureManager textureManager,
+ scoped in STGMEDIUM stgm,
+ CancellationToken ct)
+ {
+ var ms = new MemoryStream();
+ switch ((TYMED)stgm.tymed)
+ {
+ case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default:
+ {
+ var pMem = GlobalLock(stgm.hGlobal);
+ if (pMem is null)
+ Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
+ try
+ {
+ var size = (int)GlobalSize(stgm.hGlobal);
+ ms.SetLength(sizeof(BITMAPFILEHEADER) + size);
+ new ReadOnlySpan(pMem, size).CopyTo(ms.GetBuffer().AsSpan(sizeof(BITMAPFILEHEADER)));
+ }
+ finally
+ {
+ GlobalUnlock(stgm.hGlobal);
+ }
+
+ break;
+ }
+
+ case TYMED.TYMED_ISTREAM when stgm.pstm is not null:
+ {
+ STATSTG stat;
+ if (stgm.pstm->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME).SUCCEEDED && stat.cbSize.QuadPart > 0)
+ ms.SetLength(sizeof(BITMAPFILEHEADER) + (int)stat.cbSize.QuadPart);
+ else
+ ms.SetLength(8192);
+
+ var offset = (uint)sizeof(BITMAPFILEHEADER);
+ for (var read = 1u; read != 0;)
+ {
+ if (offset == ms.Length)
+ ms.SetLength(ms.Length * 2);
+ fixed (byte* pMem = ms.GetBuffer().AsSpan((int)offset))
+ {
+ stgm.pstm->Read(pMem, (uint)(ms.Length - offset), &read).ThrowOnError();
+ offset += read;
+ }
+ }
+
+ ms.SetLength(offset);
+ break;
+ }
+
+ default:
+ return Task.FromException(new NotSupportedException());
+ }
+
+ ref var bfh = ref Unsafe.As(ref ms.GetBuffer()[0]);
+ bfh.bfType = 0x4D42;
+ bfh.bfSize = (uint)ms.Length;
+
+ ref var bih = ref Unsafe.As(ref ms.GetBuffer()[sizeof(BITMAPFILEHEADER)]);
+ bfh.bfOffBits = (uint)(sizeof(BITMAPFILEHEADER) + bih.biSize);
+
+ if (bih.biSize >= sizeof(BITMAPINFOHEADER))
+ {
+ if (bih.biBitCount > 8)
+ {
+ if (bih.biCompression == BI.BI_BITFIELDS)
+ bfh.bfOffBits += (uint)(3 * sizeof(RGBQUAD));
+ else if (bih.biCompression == 6 /* BI_ALPHABITFIELDS */)
+ bfh.bfOffBits += (uint)(4 * sizeof(RGBQUAD));
+ }
+ }
+
+ if (bih.biClrUsed > 0)
+ bfh.bfOffBits += (uint)(bih.biClrUsed * sizeof(RGBQUAD));
+ else if (bih.biBitCount <= 8)
+ bfh.bfOffBits += (uint)(sizeof(RGBQUAD) << bih.biBitCount);
+
+ using var pinned = ms.GetBuffer().AsMemory().Pin();
+ using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pinned, (int)ms.Length);
+ return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
+ }
+
+ // Interprets a data as an image file using WIC.
+ static unsafe Task CreateTextureFromStorageMedium(
+ TextureManager textureManager,
+ scoped in STGMEDIUM stgm,
+ CancellationToken ct)
+ {
+ switch ((TYMED)stgm.tymed)
+ {
+ case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default:
+ {
+ var pMem = GlobalLock(stgm.hGlobal);
+ if (pMem is null)
+ Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
+ try
+ {
+ var size = (int)GlobalSize(stgm.hGlobal);
+ using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pMem, size);
+ return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
+ }
+ finally
+ {
+ GlobalUnlock(stgm.hGlobal);
+ }
+ }
+
+ case TYMED.TYMED_FILE when stgm.lpszFileName is not null:
+ {
+ var fileName = MemoryHelper.ReadString((nint)stgm.lpszFileName, Encoding.Unicode, short.MaxValue);
+ return textureManager.NoThrottleCreateFromFileAsync(fileName, ct);
+ }
+
+ case TYMED.TYMED_ISTREAM when stgm.pstm is not null:
+ {
+ using var strm = new ComPtr(stgm.pstm);
+ return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
+ }
+
+ default:
+ return Task.FromException(new NotSupportedException());
+ }
+ }
+
+ static unsafe bool TryGetClipboardDataAs(
+ ComPtr pdo,
+ uint clipboardFormat,
+ uint tymed,
+ out STGMEDIUM stgm)
+ {
+ var fec = new FORMATETC
+ {
+ cfFormat = (ushort)clipboardFormat,
+ ptd = null,
+ dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
+ lindex = -1,
+ tymed = tymed,
+ };
+ fixed (STGMEDIUM* pstgm = &stgm)
+ return pdo.Get()->GetData(&fec, pstgm).SUCCEEDED;
+ }
+
+ // Takes a data from clipboard for use with WIC.
+ static unsafe (STGMEDIUM Stgm, uint ClipboardFormat) GetSupportedClipboardData()
+ {
+ using var pdo = StaThreadService.OleGetClipboard();
+ const uint tymeds = (uint)TYMED.TYMED_HGLOBAL |
+ (uint)TYMED.TYMED_FILE |
+ (uint)TYMED.TYMED_ISTREAM;
+ const uint sharedRead = STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE;
+
+ // Try taking data from clipboard as-is.
+ if (TryGetClipboardDataAs(pdo, CF.CF_DIBV5, tymeds, out var stgm))
+ return (stgm, CF.CF_DIBV5);
+ if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileContents, tymeds, out stgm))
+ return (stgm, ClipboardFormats.FileContents);
+ if (TryGetClipboardDataAs(pdo, ClipboardFormats.Png, tymeds, out stgm))
+ return (stgm, ClipboardFormats.Png);
+ if (TryGetClipboardDataAs(pdo, CF.CF_DIB, tymeds, out stgm))
+ return (stgm, CF.CF_DIB);
+
+ // Try reading file from the path stored in clipboard.
+ if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameW, (uint)TYMED.TYMED_HGLOBAL, out stgm))
+ {
+ var pPath = GlobalLock(stgm.hGlobal);
+ try
+ {
+ IStream* pfs;
+ SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError();
+
+ var stgm2 = new STGMEDIUM
+ {
+ tymed = (uint)TYMED.TYMED_ISTREAM,
+ pstm = pfs,
+ pUnkForRelease = (IUnknown*)pfs,
+ };
+ return (stgm2, ClipboardFormats.FileContents);
+ }
+ finally
+ {
+ if (pPath is not null)
+ GlobalUnlock(stgm.hGlobal);
+ StaThreadService.ReleaseStgMedium(ref stgm);
+ }
+ }
+
+ if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameA, (uint)TYMED.TYMED_HGLOBAL, out stgm))
+ {
+ var pPath = GlobalLock(stgm.hGlobal);
+ try
+ {
+ IStream* pfs;
+ SHCreateStreamOnFileA((sbyte*)pPath, sharedRead, &pfs).ThrowOnError();
+
+ var stgm2 = new STGMEDIUM
+ {
+ tymed = (uint)TYMED.TYMED_ISTREAM,
+ pstm = pfs,
+ pUnkForRelease = (IUnknown*)pfs,
+ };
+ return (stgm2, ClipboardFormats.FileContents);
+ }
+ finally
+ {
+ if (pPath is not null)
+ GlobalUnlock(stgm.hGlobal);
+ StaThreadService.ReleaseStgMedium(ref stgm);
+ }
+ }
+
+ throw new InvalidOperationException("No compatible clipboard format found.");
+ }
+ }
+}
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs
index 245a2a9acd..700057e8c9 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs
@@ -6,7 +6,6 @@
using System.Threading;
using System.Threading.Tasks;
-using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
@@ -173,7 +172,7 @@ internal IDalamudTextureWrap NoThrottleCreateFromImage(
ReadOnlyMemory bytes,
CancellationToken cancellationToken = default)
{
- ObjectDisposedException.ThrowIf(this.disposing, this);
+ ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested();
try
@@ -204,7 +203,7 @@ internal async Task NoThrottleCreateFromFileAsync(
string path,
CancellationToken cancellationToken = default)
{
- ObjectDisposedException.ThrowIf(this.disposing, this);
+ ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested();
try
@@ -359,11 +358,18 @@ public void Dispose()
/// An instance of .
/// The number of bytes in the memory.
/// The new instance of .
- public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length)
+ public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) =>
+ this.CreateIStreamViewOfMemory((byte*)handle.Pointer, length);
+
+ /// Creates a new instance of from a fixed memory allocation.
+ /// Address of the data.
+ /// The number of bytes in the memory.
+ /// The new instance of .
+ public unsafe ComPtr CreateIStreamViewOfMemory(void* address, int length)
{
using var wicStream = default(ComPtr);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
- wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError();
+ wicStream.Get()->InitializeFromMemory((byte*)address, checked((uint)length)).ThrowOnError();
var res = default(ComPtr);
wicStream.As(ref res).ThrowOnError();
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs
index c9ee5d20e2..8ac8e60ec3 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs
@@ -11,7 +11,9 @@
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom;
@@ -48,10 +50,11 @@ internal sealed partial class TextureManager
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service.Get();
+ private readonly CancellationTokenSource disposeCts = new();
+
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager;
- private bool disposing;
private ComPtr device;
[ServiceManager.ServiceConstructor]
@@ -104,10 +107,10 @@ public WicManager Wic
///
void IInternalDisposableService.DisposeService()
{
- if (this.disposing)
+ if (this.disposeCts.IsCancellationRequested)
return;
- this.disposing = true;
+ this.disposeCts.Cancel();
Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose();
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
@@ -269,6 +272,21 @@ public unsafe IDalamudTextureWrap CreateEmpty(
return wrap;
}
+ ///
+ public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName);
+
+ ///
+ /// Plugin that created the draw list.
+ ///
+ ///
+ public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) =>
+ new DrawListTextureWrap(
+ new(this.device),
+ this,
+ Service.Get().Empty4X4,
+ plugin,
+ debugName ?? $"{nameof(this.CreateDrawListTexture)}");
+
///
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
@@ -330,7 +348,7 @@ internal unsafe IDalamudTextureWrap NoThrottleCreateFromRaw(
/// The loaded texture.
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file)
{
- ObjectDisposedException.ThrowIf(this.disposing, this);
+ ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var buffer = file.TextureBuffer;
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
@@ -354,7 +372,7 @@ internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file)
/// The loaded texture.
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes)
{
- ObjectDisposedException.ThrowIf(this.disposing, this);
+ ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile.");
diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
index 68e2dde47e..2b9f5b506f 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
@@ -147,6 +147,10 @@ public IDalamudTextureWrap CreateEmpty(
return textureWrap;
}
+ ///
+ public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) =>
+ this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName);
+
///
public async Task CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
@@ -263,6 +267,17 @@ public async Task CreateFromTexFileAsync(
return textureWrap;
}
+ ///
+ public async Task CreateFromClipboardAsync(
+ string? debugName = null,
+ CancellationToken cancellationToken = default)
+ {
+ var manager = await this.ManagerTask;
+ var textureWrap = await manager.CreateFromClipboardAsync(debugName, cancellationToken);
+ manager.Blame(textureWrap, this.plugin);
+ return textureWrap;
+ }
+
///
public IEnumerable GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
@@ -275,6 +290,9 @@ public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup)
return shared;
}
+ ///
+ public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage();
+
///
public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture)
{
@@ -407,6 +425,17 @@ await manager.SaveToFileAsync(
cancellationToken);
}
+ ///
+ public async Task CopyToClipboardAsync(
+ IDalamudTextureWrap wrap,
+ string? preferredFileNameWithoutExtension = null,
+ bool leaveWrapOpen = false,
+ CancellationToken cancellationToken = default)
+ {
+ var manager = await this.ManagerTask;
+ await manager.CopyToClipboardAsync(wrap, preferredFileNameWithoutExtension, leaveWrapOpen, cancellationToken);
+ }
+
private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) =>
this.InterceptTexDataLoad?.Invoke(path, ref replacementPath);
}
diff --git a/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs
new file mode 100644
index 0000000000..4fb5d1acad
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs
@@ -0,0 +1,61 @@
+using System.Numerics;
+
+using Dalamud.Interface.Textures.TextureWraps.Internal;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Textures.TextureWraps;
+
+/// A texture wrap that can be drawn using ImGui draw data.
+public interface IDrawListTextureWrap : IDalamudTextureWrap
+{
+ /// Gets or sets the width of the texture.
+ /// If is to be set together, set use instead.
+ new int Width { get; set; }
+
+ /// Gets or sets the width of the texture.
+ /// If is to be set together, set use instead.
+ new int Height { get; set; }
+
+ /// Gets or sets the size of the texture.
+ /// Components will be rounded up.
+ new Vector2 Size { get; set; }
+
+ ///
+ int IDalamudTextureWrap.Width => this.Width;
+
+ ///
+ int IDalamudTextureWrap.Height => this.Height;
+
+ ///
+ Vector2 IDalamudTextureWrap.Size => this.Size;
+
+ /// Gets or sets the color to use when clearing this texture.
+ /// Color in RGBA. Defaults to , which is full transparency.
+ Vector4 ClearColor { get; set; }
+
+ /// Draws a draw list to this texture.
+ /// Draw list to draw from.
+ /// Left-top coordinates of the draw commands in the draw list.
+ /// Scale to apply to all draw commands in the draw list.
+ /// This function can be called only from the main thread.
+ void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale);
+
+ ///
+ void Draw(scoped in ImDrawData drawData);
+
+ /// Draws from a draw data to this texture.
+ /// Draw data to draw.
+ ///
+ /// - Texture size will be kept as specified in . will be
+ /// used only as shader parameters.
+ /// - This function can be called only from the main thread.
+ ///
+ void Draw(ImDrawDataPtr drawData);
+
+ /// Resizes this texture and draws an ImGui window.
+ /// Name and ID of the window to draw. Use the value that goes into
+ /// .
+ /// Scale to apply to all draw commands in the draw list.
+ void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale);
+}
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs
new file mode 100644
index 0000000000..4e82479b03
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs
@@ -0,0 +1,283 @@
+using System.Numerics;
+
+using Dalamud.Interface.Internal;
+using Dalamud.Interface.Textures.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.Textures.TextureWraps.Internal;
+
+///
+internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, IDeferredDisposable
+{
+ private readonly TextureManager textureManager;
+ private readonly IDalamudTextureWrap emptyTexture;
+ private readonly LocalPlugin? plugin;
+ private readonly string debugName;
+
+ private ComPtr device;
+ private ComPtr deviceContext;
+ private ComPtr tex;
+ private ComPtr srv;
+ private ComPtr rtv;
+ private ComPtr uav;
+
+ private int width;
+ private int height;
+ private DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM;
+
+ /// Initializes a new instance of the class.
+ /// Pointer to a D3D11 device. Ownership is taken.
+ /// Instance of the class.
+ /// Texture to use, if or is 0.
+ /// Plugin that holds responsible for this texture.
+ /// Name for debug display purposes.
+ public DrawListTextureWrap(
+ ComPtr device,
+ TextureManager textureManager,
+ IDalamudTextureWrap emptyTexture,
+ LocalPlugin? plugin,
+ string debugName)
+ {
+ this.textureManager = textureManager;
+ this.emptyTexture = emptyTexture;
+ this.plugin = plugin;
+ this.debugName = debugName;
+
+ if (device.IsEmpty())
+ throw new ArgumentNullException(nameof(device));
+
+ this.device.Swap(ref device);
+ fixed (ID3D11DeviceContext** pdc = &this.deviceContext.GetPinnableReference())
+ this.device.Get()->GetImmediateContext(pdc);
+
+ this.emptyTexture = emptyTexture;
+ this.srv = new((ID3D11ShaderResourceView*)emptyTexture.ImGuiHandle);
+ }
+
+ /// Finalizes an instance of the class.
+ ~DrawListTextureWrap() => this.RealDispose();
+
+ ///
+ public nint ImGuiHandle => (nint)this.srv.Get();
+
+ ///
+ public int Width
+ {
+ get => this.width;
+ set
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value));
+ this.Resize(value, this.height, this.format);
+ }
+ }
+
+ ///
+ public int Height
+ {
+ get => this.height;
+ set
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value));
+ this.Resize(this.width, value, this.format).ThrowOnError();
+ }
+ }
+
+ ///
+ public Vector2 Size
+ {
+ get => new(this.width, this.height);
+ set
+ {
+ if (value.X is <= 0 or float.NaN)
+ throw new ArgumentOutOfRangeException(nameof(value), value, "X component is invalid.");
+ if (value.Y is <= 0 or float.NaN)
+ throw new ArgumentOutOfRangeException(nameof(value), value, "Y component is invalid.");
+ this.Resize((int)MathF.Ceiling(value.X), (int)MathF.Ceiling(value.Y), this.format).ThrowOnError();
+ }
+ }
+
+ ///
+ public Vector4 ClearColor { get; set; }
+
+ /// Gets or sets the .
+ public int DxgiFormat
+ {
+ get => (int)this.format;
+ set
+ {
+ if (!this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)value))
+ {
+ throw new ArgumentException(
+ "Specified format is not a supported rendering target format.",
+ nameof(value));
+ }
+
+ this.Resize(this.width, this.Height, (DXGI_FORMAT)value).ThrowOnError();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (Service.GetNullable() is { } im)
+ im.EnqueueDeferredDispose(this);
+ else
+ this.RealDispose();
+ }
+
+ ///
+ public void RealDispose()
+ {
+ this.srv.Reset();
+ this.tex.Reset();
+ this.rtv.Reset();
+ this.uav.Reset();
+ this.device.Reset();
+ this.deviceContext.Reset();
+
+#pragma warning disable CA1816
+ GC.SuppressFinalize(this);
+#pragma warning restore CA1816
+ }
+
+ ///
+ public void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale) =>
+ this.Draw(
+ new ImDrawData
+ {
+ Valid = 1,
+ CmdListsCount = 1,
+ TotalIdxCount = drawListPtr.IdxBuffer.Size,
+ TotalVtxCount = drawListPtr.VtxBuffer.Size,
+ CmdLists = (ImDrawList**)(&drawListPtr),
+ DisplayPos = displayPos,
+ DisplaySize = this.Size,
+ FramebufferScale = scale,
+ });
+
+ ///
+ public void Draw(scoped in ImDrawData drawData)
+ {
+ fixed (ImDrawData* pDrawData = &drawData)
+ this.Draw(new(pDrawData));
+ }
+
+ ///
+ public void Draw(ImDrawDataPtr drawData)
+ {
+ ThreadSafety.AssertMainThread();
+
+ // Do nothing if the render target is empty.
+ if (this.rtv.IsEmpty())
+ return;
+
+ // Clear the texture first, as the texture exists.
+ var clearColor = this.ClearColor;
+ this.deviceContext.Get()->ClearRenderTargetView(this.rtv.Get(), (float*)&clearColor);
+
+ // If there is nothing to draw, then stop.
+ if (!drawData.Valid
+ || drawData.CmdListsCount < 1
+ || drawData.TotalIdxCount < 1
+ || drawData.TotalVtxCount < 1
+ || drawData.CmdLists == 0
+ || drawData.DisplaySize.X <= 0
+ || drawData.DisplaySize.Y <= 0
+ || drawData.FramebufferScale.X == 0
+ || drawData.FramebufferScale.Y == 0)
+ return;
+
+ using (new DeviceContextStateBackup(this.device.Get()->GetFeatureLevel(), this.deviceContext))
+ {
+ Service.Get().RenderDrawData(this.rtv.Get(), drawData);
+ Service.Get().MakeStraight(this.uav.Get());
+ }
+ }
+
+ /// Resizes the texture.
+ /// New texture width.
+ /// New texture height.
+ /// New format.
+ /// if the texture has been resized, if the texture has not
+ /// been resized, or a value with that evaluates to .
+ private HRESULT Resize(int newWidth, int newHeight, DXGI_FORMAT newFormat)
+ {
+ if (newWidth < 0 || newHeight < 0)
+ return E.E_INVALIDARG;
+
+ if (newWidth == 0 || newHeight == 0)
+ {
+ this.tex.Reset();
+ this.srv.Reset();
+ this.rtv.Reset();
+ this.uav.Reset();
+ this.width = newWidth;
+ this.Height = newHeight;
+ this.srv = new((ID3D11ShaderResourceView*)this.emptyTexture.ImGuiHandle);
+ return S.S_FALSE;
+ }
+
+ if (this.width == newWidth && this.height == newHeight)
+ return S.S_FALSE;
+
+ // These new resources will take replace the existing resources, only once all allocations are completed.
+ using var tmptex = default(ComPtr);
+ using var tmpsrv = default(ComPtr);
+ using var tmprtv = default(ComPtr);
+ using var tmpuav = default(ComPtr);
+
+ var tmpTexDesc = new D3D11_TEXTURE2D_DESC
+ {
+ Width = (uint)newWidth,
+ Height = (uint)newHeight,
+ MipLevels = 1,
+ ArraySize = 1,
+ Format = newFormat,
+ SampleDesc = new(1, 0),
+ Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT,
+ BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE |
+ D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET |
+ D3D11_BIND_FLAG.D3D11_BIND_UNORDERED_ACCESS),
+ CPUAccessFlags = 0u,
+ MiscFlags = 0u,
+ };
+ var hr = this.device.Get()->CreateTexture2D(&tmpTexDesc, null, tmptex.GetAddressOf());
+ if (hr.FAILED)
+ return hr;
+
+ var tmpres = (ID3D11Resource*)tmptex.Get();
+ var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC(tmptex, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D);
+ hr = this.device.Get()->CreateShaderResourceView(tmpres, &srvDesc, tmpsrv.GetAddressOf());
+ if (hr.FAILED)
+ return hr;
+
+ var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC(tmptex, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D);
+ hr = this.device.Get()->CreateRenderTargetView(tmpres, &rtvDesc, tmprtv.GetAddressOf());
+ if (hr.FAILED)
+ return hr;
+
+ var uavDesc = new D3D11_UNORDERED_ACCESS_VIEW_DESC(tmptex, D3D11_UAV_DIMENSION.D3D11_UAV_DIMENSION_TEXTURE2D);
+ hr = this.device.Get()->CreateUnorderedAccessView(tmpres, &uavDesc, tmpuav.GetAddressOf());
+ if (hr.FAILED)
+ return hr;
+
+ tmptex.Swap(ref this.tex);
+ tmpsrv.Swap(ref this.srv);
+ tmprtv.Swap(ref this.rtv);
+ tmpuav.Swap(ref this.uav);
+ this.width = newWidth;
+ this.height = newHeight;
+ this.format = newFormat;
+
+ this.textureManager.BlameSetName(this, this.debugName);
+ this.textureManager.Blame(this, this.plugin);
+ return S.S_OK;
+ }
+}
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs
new file mode 100644
index 0000000000..55cf138815
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs
@@ -0,0 +1,669 @@
+using System.Runtime.InteropServices;
+
+using ImGuiNET;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.Textures.TextureWraps.Internal;
+
+///
+internal sealed unsafe partial class DrawListTextureWrap
+{
+ /// Captures states of a .
+ // TODO: Use the one in https://github.com/goatcorp/Dalamud/pull/1923 once the PR goes in
+ internal struct DeviceContextStateBackup : IDisposable
+ {
+ private InputAssemblerState inputAssemblerState;
+ private RasterizerState rasterizerState;
+ private OutputMergerState outputMergerState;
+ private VertexShaderState vertexShaderState;
+ private HullShaderState hullShaderState;
+ private DomainShaderState domainShaderState;
+ private GeometryShaderState geometryShaderState;
+ private PixelShaderState pixelShaderState;
+ private ComputeShaderState computeShaderState;
+
+ ///
+ /// Initializes a new instance of the struct,
+ /// by capturing all states of a .
+ ///
+ /// The feature level.
+ /// The device context.
+ public DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ this.inputAssemblerState = InputAssemblerState.From(ctx);
+ this.rasterizerState = RasterizerState.From(ctx);
+ this.outputMergerState = OutputMergerState.From(featureLevel, ctx);
+ this.vertexShaderState = VertexShaderState.From(ctx);
+ this.hullShaderState = HullShaderState.From(ctx);
+ this.domainShaderState = DomainShaderState.From(ctx);
+ this.geometryShaderState = GeometryShaderState.From(ctx);
+ this.pixelShaderState = PixelShaderState.From(ctx);
+ this.computeShaderState = ComputeShaderState.From(featureLevel, ctx);
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.inputAssemblerState.Dispose();
+ this.rasterizerState.Dispose();
+ this.outputMergerState.Dispose();
+ this.vertexShaderState.Dispose();
+ this.hullShaderState.Dispose();
+ this.domainShaderState.Dispose();
+ this.geometryShaderState.Dispose();
+ this.pixelShaderState.Dispose();
+ this.computeShaderState.Dispose();
+ }
+
+ ///
+ /// Captures Input Assembler states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct InputAssemblerState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr layout;
+ private ComPtr indexBuffer;
+ private DXGI_FORMAT indexFormat;
+ private uint indexOffset;
+ private D3D_PRIMITIVE_TOPOLOGY topology;
+ private fixed ulong buffers[BufferCount];
+ private fixed uint strides[BufferCount];
+ private fixed uint offsets[BufferCount];
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static InputAssemblerState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(InputAssemblerState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->IAGetInputLayout(state.layout.GetAddressOf());
+ ctx->IAGetPrimitiveTopology(&state.topology);
+ ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset);
+ ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (InputAssemblerState* pThis = &this)
+ {
+ ctx->IASetInputLayout(pThis->layout);
+ ctx->IASetPrimitiveTopology(pThis->topology);
+ ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset);
+ ctx->IASetVertexBuffers(
+ 0,
+ BufferCount,
+ (ID3D11Buffer**)pThis->buffers,
+ pThis->strides,
+ pThis->offsets);
+
+ pThis->context.Dispose();
+ pThis->layout.Dispose();
+ pThis->indexBuffer.Dispose();
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Rasterizer states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct RasterizerState : IDisposable
+ {
+ private const int Count = D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX;
+
+ private ComPtr context;
+ private ComPtr state;
+ private fixed byte viewports[24 * Count];
+ private fixed ulong scissorRects[16 * Count];
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static RasterizerState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(RasterizerState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->RSGetState(state.state.GetAddressOf());
+ uint n = Count;
+ ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports);
+ n = Count;
+ ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (RasterizerState* pThis = &this)
+ {
+ ctx->RSSetState(pThis->state);
+ ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports);
+ ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects);
+
+ pThis->context.Dispose();
+ pThis->state.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Output Merger states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct OutputMergerState : IDisposable
+ {
+ private const int RtvCount = D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT;
+ private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr blendState;
+ private fixed float blendFactor[4];
+ private uint sampleMask;
+ private uint stencilRef;
+ private ComPtr depthStencilState;
+ private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount]
+ private ComPtr dsv;
+ private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount]
+ private int uavCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The feature level.
+ /// The device context.
+ /// The captured state.
+ public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ var state = default(OutputMergerState);
+ state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
+ ? D3D11.D3D11_1_UAV_SLOT_COUNT
+ : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask);
+ ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef);
+ ctx->OMGetRenderTargetsAndUnorderedAccessViews(
+ RtvCount,
+ (ID3D11RenderTargetView**)state.rtvs,
+ state.dsv.GetAddressOf(),
+ 0,
+ (uint)state.uavCount,
+ (ID3D11UnorderedAccessView**)state.uavs);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (OutputMergerState* pThis = &this)
+ {
+ ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask);
+ ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef);
+ var rtvc = (uint)RtvCount;
+ while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0)
+ rtvc--;
+
+ var uavlb = rtvc;
+ while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0)
+ uavlb++;
+
+ var uavc = (uint)this.uavCount;
+ while (uavc > uavlb && pThis->uavs[uavc - 1] == 0)
+ uavlb--;
+ uavc -= uavlb;
+
+ ctx->OMSetRenderTargetsAndUnorderedAccessViews(
+ rtvc,
+ (ID3D11RenderTargetView**)pThis->rtvs,
+ pThis->dsv,
+ uavc == 0 ? 0 : uavlb,
+ uavc,
+ uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs,
+ null);
+
+ this.context.Reset();
+ this.blendState.Reset();
+ this.depthStencilState.Reset();
+ this.dsv.Reset();
+ foreach (ref var b in new Span>(pThis->rtvs, RtvCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->uavs, this.uavCount))
+ b.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Vertex Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct VertexShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static VertexShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(VertexShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (VertexShaderState* pThis = &this)
+ {
+ ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Hull Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct HullShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static HullShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(HullShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (HullShaderState* pThis = &this)
+ {
+ ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Domain Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DomainShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static DomainShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(DomainShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (DomainShaderState* pThis = &this)
+ {
+ ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Geometry Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct GeometryShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static GeometryShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(GeometryShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (GeometryShaderState* pThis = &this)
+ {
+ ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Pixel Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct PixelShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static PixelShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(PixelShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (PixelShaderState* pThis = &this)
+ {
+ ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Compute Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct ComputeShaderState : IDisposable
+ {
+ private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int InstanceCount = 256; // According to msdn
+ private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount]
+ private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount]
+ private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount]
+ private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount]
+ private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax]
+ private uint instCount;
+ private int uavCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The feature level.
+ /// The device context.
+ /// The captured state.
+ public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ var state = default(ComputeShaderState);
+ state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
+ ? D3D11.D3D11_1_UAV_SLOT_COUNT
+ : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = InstanceCount;
+ ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (ComputeShaderState* pThis = &this)
+ {
+ ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+ ctx->CSSetUnorderedAccessViews(
+ 0,
+ (uint)this.uavCount,
+ (ID3D11UnorderedAccessView**)pThis->uavs,
+ null);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->uavs, this.uavCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl
new file mode 100644
index 0000000000..17b53ba6c8
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl
@@ -0,0 +1,4 @@
+cbuffer TransformationBuffer : register(b0) {
+ float4x4 g_view;
+ float4 g_colorMultiplier;
+}
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl
new file mode 100644
index 0000000000..171d3e73be
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl
@@ -0,0 +1,40 @@
+#include "DrawListTexture.Renderer.Common.hlsl"
+
+struct ImDrawVert {
+ float2 position : POSITION;
+ float2 uv : TEXCOORD0;
+ float4 color : COLOR0;
+};
+
+struct VsData {
+ float4 position : SV_POSITION;
+ float2 uv : TEXCOORD0;
+ float4 color : COLOR0;
+};
+
+struct PsData {
+ float4 color : COLOR0;
+};
+
+Texture2D s_texture : register(t0);
+SamplerState s_sampler : register(s0);
+RWTexture2D s_output : register(u1);
+
+VsData vs_main(const ImDrawVert idv) {
+ VsData result;
+ result.position = mul(g_view, float4(idv.position, 0, 1));
+ result.uv = idv.uv;
+ result.color = idv.color;
+ return result;
+}
+
+float4 ps_main(const VsData vd) : SV_TARGET {
+ return s_texture.Sample(s_sampler, vd.uv) * vd.color;
+}
+
+/*
+
+fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.DrawToPremul.vs.bin DrawListTexture.Renderer.DrawToPremul.hlsl
+fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.DrawToPremul.ps.bin DrawListTexture.Renderer.DrawToPremul.hlsl
+
+*/
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin
new file mode 100644
index 0000000000..e3c68edf33
Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin differ
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin
new file mode 100644
index 0000000000..0079755c0e
Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin differ
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl
new file mode 100644
index 0000000000..b8423697a1
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl
@@ -0,0 +1,22 @@
+RWTexture2D s_output : register(u1);
+
+float4 vs_main(const float2 position : POSITION) : SV_POSITION {
+ return float4(position, 0, 1);
+}
+
+float4 ps_main(const float4 position : SV_POSITION) : SV_TARGET {
+ const float4 src = s_output[position.xy];
+ s_output[position.xy] =
+ src.a > 0
+ ? float4(src.rgb / src.a, src.a)
+ : float4(0, 0, 0, 0);
+
+ return float4(0, 0, 0, 0); // unused
+}
+
+/*
+
+fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.MakeStraight.vs.bin DrawListTexture.Renderer.MakeStraight.hlsl
+fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.MakeStraight.ps.bin DrawListTexture.Renderer.MakeStraight.hlsl
+
+*/
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin
new file mode 100644
index 0000000000..0b979f6b61
Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin differ
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin
new file mode 100644
index 0000000000..1baeecdaef
Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin differ
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs
new file mode 100644
index 0000000000..cc6cfd000b
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs
@@ -0,0 +1,595 @@
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+using Dalamud.Interface.Internal;
+using Dalamud.Interface.Utility;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.Textures.TextureWraps.Internal;
+
+///
+internal sealed unsafe partial class DrawListTextureWrap
+{
+ /// The renderer.
+ [ServiceManager.EarlyLoadedService]
+ internal sealed class Renderer : IInternalDisposableService
+ {
+ private ComPtr device;
+ private ComPtr deviceContext;
+
+ private ComPtr drawToPremulVertexShader;
+ private ComPtr drawToPremulPixelShader;
+ private ComPtr drawToPremulInputLayout;
+ private ComPtr drawToPremulVertexBuffer;
+ private ComPtr drawToPremulVertexConstantBuffer;
+ private ComPtr drawToPremulIndexBuffer;
+
+ private ComPtr makeStraightVertexShader;
+ private ComPtr makeStraightPixelShader;
+ private ComPtr makeStraightInputLayout;
+ private ComPtr makeStraightVertexBuffer;
+ private ComPtr makeStraightIndexBuffer;
+
+ private ComPtr samplerState;
+ private ComPtr blendState;
+ private ComPtr rasterizerState;
+ private ComPtr depthStencilState;
+ private int vertexBufferSize;
+ private int indexBufferSize;
+
+ [ServiceManager.ServiceConstructor]
+ private Renderer(InterfaceManager.InterfaceManagerWithScene iwms)
+ {
+ try
+ {
+ this.device = new((ID3D11Device*)iwms.Manager.Device!.NativePointer);
+ fixed (ID3D11DeviceContext** p = &this.deviceContext.GetPinnableReference())
+ this.device.Get()->GetImmediateContext(p);
+ this.deviceContext.Get()->AddRef();
+
+ this.Setup();
+ }
+ catch
+ {
+ this.ReleaseUnmanagedResources();
+ throw;
+ }
+ }
+
+ /// Finalizes an instance of the class.
+ ~Renderer() => this.ReleaseUnmanagedResources();
+
+ ///
+ public void DisposeService() => this.ReleaseUnmanagedResources();
+
+ /// Renders draw data.
+ /// The render target.
+ /// Pointer to the draw data.
+ public void RenderDrawData(ID3D11RenderTargetView* prtv, ImDrawDataPtr drawData)
+ {
+ ThreadSafety.AssertMainThread();
+
+ if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0
+ || !drawData.Valid || drawData.CmdListsCount < 1)
+ return;
+ var cmdLists = new Span(drawData.NativePtr->CmdLists, drawData.NativePtr->CmdListsCount);
+
+ // Create and grow vertex/index buffers if needed
+ if (this.vertexBufferSize < drawData.TotalVtxCount)
+ this.drawToPremulVertexBuffer.Dispose();
+ if (this.drawToPremulVertexBuffer.Get() is null)
+ {
+ this.vertexBufferSize = drawData.TotalVtxCount + 5000;
+ var desc = new D3D11_BUFFER_DESC(
+ (uint)(sizeof(ImDrawVert) * this.vertexBufferSize),
+ (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
+ D3D11_USAGE.D3D11_USAGE_DYNAMIC,
+ (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE);
+ var buffer = default(ID3D11Buffer*);
+ this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError();
+ this.drawToPremulVertexBuffer.Attach(buffer);
+ }
+
+ if (this.indexBufferSize < drawData.TotalIdxCount)
+ this.drawToPremulIndexBuffer.Dispose();
+ if (this.drawToPremulIndexBuffer.Get() is null)
+ {
+ this.indexBufferSize = drawData.TotalIdxCount + 5000;
+ var desc = new D3D11_BUFFER_DESC(
+ (uint)(sizeof(ushort) * this.indexBufferSize),
+ (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER,
+ D3D11_USAGE.D3D11_USAGE_DYNAMIC,
+ (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE);
+ var buffer = default(ID3D11Buffer*);
+ this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError();
+ this.drawToPremulIndexBuffer.Attach(buffer);
+ }
+
+ // Upload vertex/index data into a single contiguous GPU buffer
+ try
+ {
+ var vertexData = default(D3D11_MAPPED_SUBRESOURCE);
+ var indexData = default(D3D11_MAPPED_SUBRESOURCE);
+ this.deviceContext.Get()->Map(
+ (ID3D11Resource*)this.drawToPremulVertexBuffer.Get(),
+ 0,
+ D3D11_MAP.D3D11_MAP_WRITE_DISCARD,
+ 0,
+ &vertexData).ThrowOnError();
+ this.deviceContext.Get()->Map(
+ (ID3D11Resource*)this.drawToPremulIndexBuffer.Get(),
+ 0,
+ D3D11_MAP.D3D11_MAP_WRITE_DISCARD,
+ 0,
+ &indexData).ThrowOnError();
+
+ var targetVertices = new Span(vertexData.pData, this.vertexBufferSize);
+ var targetIndices = new Span(indexData.pData, this.indexBufferSize);
+ foreach (ref var cmdList in cmdLists)
+ {
+ var vertices = new ImVectorWrapper(&cmdList.NativePtr->VtxBuffer);
+ var indices = new ImVectorWrapper(&cmdList.NativePtr->IdxBuffer);
+
+ vertices.DataSpan.CopyTo(targetVertices);
+ indices.DataSpan.CopyTo(targetIndices);
+
+ targetVertices = targetVertices[vertices.Length..];
+ targetIndices = targetIndices[indices.Length..];
+ }
+ }
+ finally
+ {
+ this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexBuffer.Get(), 0);
+ this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulIndexBuffer.Get(), 0);
+ }
+
+ // Setup orthographic projection matrix into our constant buffer.
+ // Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB).
+ // DisplayPos is (0,0) for single viewport apps.
+ try
+ {
+ var data = default(D3D11_MAPPED_SUBRESOURCE);
+ this.deviceContext.Get()->Map(
+ (ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(),
+ 0,
+ D3D11_MAP.D3D11_MAP_WRITE_DISCARD,
+ 0,
+ &data).ThrowOnError();
+ ref var xform = ref *(TransformationBuffer*)data.pData;
+ xform.View =
+ Matrix4x4.CreateOrthographicOffCenter(
+ drawData.DisplayPos.X,
+ drawData.DisplayPos.X + drawData.DisplaySize.X,
+ drawData.DisplayPos.Y + drawData.DisplaySize.Y,
+ drawData.DisplayPos.Y,
+ 1f,
+ 0f);
+ }
+ finally
+ {
+ this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(), 0);
+ }
+
+ // Set up render state
+ {
+ this.deviceContext.Get()->IASetInputLayout(this.drawToPremulInputLayout);
+ var buffer = this.drawToPremulVertexBuffer.Get();
+ var stride = (uint)sizeof(ImDrawVert);
+ var offset = 0u;
+ this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
+ this.deviceContext.Get()->IASetIndexBuffer(
+ this.drawToPremulIndexBuffer,
+ DXGI_FORMAT.DXGI_FORMAT_R16_UINT,
+ 0);
+ this.deviceContext.Get()->IASetPrimitiveTopology(
+ D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ var viewport = new D3D11_VIEWPORT(
+ 0,
+ 0,
+ drawData.DisplaySize.X * drawData.FramebufferScale.X,
+ drawData.DisplaySize.Y * drawData.FramebufferScale.Y);
+ this.deviceContext.Get()->RSSetState(this.rasterizerState);
+ this.deviceContext.Get()->RSSetViewports(1, &viewport);
+
+ var blendColor = default(Vector4);
+ this.deviceContext.Get()->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff);
+ this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0);
+ this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null);
+
+ this.deviceContext.Get()->VSSetShader(this.drawToPremulVertexShader.Get(), null, 0);
+ buffer = this.drawToPremulVertexConstantBuffer.Get();
+ this.deviceContext.Get()->VSSetConstantBuffers(0, 1, &buffer);
+
+ // PS handled later
+
+ this.deviceContext.Get()->GSSetShader(null, null, 0);
+ this.deviceContext.Get()->HSSetShader(null, null, 0);
+ this.deviceContext.Get()->DSSetShader(null, null, 0);
+ this.deviceContext.Get()->CSSetShader(null, null, 0);
+ }
+
+ // Render command lists
+ // (Because we merged all buffers into a single one, we maintain our own offset into them)
+ var vertexOffset = 0;
+ var indexOffset = 0;
+ var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y);
+ var frameBufferScaleV4 =
+ new Vector4(drawData.FramebufferScale, drawData.FramebufferScale.X, drawData.FramebufferScale.Y);
+ foreach (ref var cmdList in cmdLists)
+ {
+ var cmds = new ImVectorWrapper(&cmdList.NativePtr->CmdBuffer);
+ foreach (ref var cmd in cmds.DataSpan)
+ {
+ var clipV4 = (cmd.ClipRect - clipOff) * frameBufferScaleV4;
+ var clipRect = new RECT((int)clipV4.X, (int)clipV4.Y, (int)clipV4.Z, (int)clipV4.W);
+
+ // Skip the draw if nothing would be visible
+ if (clipRect.left >= clipRect.right || clipRect.top >= clipRect.bottom || cmd.ElemCount == 0)
+ continue;
+
+ this.deviceContext.Get()->RSSetScissorRects(1, &clipRect);
+
+ if (cmd.UserCallback == nint.Zero)
+ {
+ // Bind texture and draw
+ var samplerp = this.samplerState.Get();
+ var srvp = (ID3D11ShaderResourceView*)cmd.TextureId;
+ this.deviceContext.Get()->PSSetShader(this.drawToPremulPixelShader, null, 0);
+ this.deviceContext.Get()->PSSetSamplers(0, 1, &samplerp);
+ this.deviceContext.Get()->PSSetShaderResources(0, 1, &srvp);
+ this.deviceContext.Get()->DrawIndexed(
+ cmd.ElemCount,
+ (uint)(cmd.IdxOffset + indexOffset),
+ (int)(cmd.VtxOffset + vertexOffset));
+ }
+ }
+
+ indexOffset += cmdList.IdxBuffer.Size;
+ vertexOffset += cmdList.VtxBuffer.Size;
+ }
+ }
+
+ /// Renders draw data.
+ /// The pointer to a Texture2D UAV to make straight.
+ public void MakeStraight(ID3D11UnorderedAccessView* puav)
+ {
+ ThreadSafety.AssertMainThread();
+
+ D3D11_TEXTURE2D_DESC texDesc;
+ using (var texRes = default(ComPtr))
+ {
+ puav->GetResource(texRes.GetAddressOf());
+
+ using var tex = default(ComPtr);
+ texRes.As(&tex).ThrowOnError();
+ tex.Get()->GetDesc(&texDesc);
+ }
+
+ this.deviceContext.Get()->IASetInputLayout(this.makeStraightInputLayout);
+ var buffer = this.makeStraightVertexBuffer.Get();
+ var stride = (uint)sizeof(Vector2);
+ var offset = 0u;
+ this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
+ this.deviceContext.Get()->IASetIndexBuffer(
+ this.makeStraightIndexBuffer,
+ DXGI_FORMAT.DXGI_FORMAT_R16_UINT,
+ 0);
+ this.deviceContext.Get()->IASetPrimitiveTopology(
+ D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ var scissorRect = new RECT(0, 0, (int)texDesc.Width, (int)texDesc.Height);
+ this.deviceContext.Get()->RSSetScissorRects(1, &scissorRect);
+ this.deviceContext.Get()->RSSetState(this.rasterizerState);
+ var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height);
+ this.deviceContext.Get()->RSSetViewports(1, &viewport);
+
+ this.deviceContext.Get()->OMSetBlendState(null, null, 0xFFFFFFFF);
+ this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0);
+ var nullrtv = default(ID3D11RenderTargetView*);
+ this.deviceContext.Get()->OMSetRenderTargetsAndUnorderedAccessViews(1, &nullrtv, null, 1, 1, &puav, null);
+
+ this.deviceContext.Get()->VSSetShader(this.makeStraightVertexShader.Get(), null, 0);
+ this.deviceContext.Get()->PSSetShader(this.makeStraightPixelShader.Get(), null, 0);
+ this.deviceContext.Get()->GSSetShader(null, null, 0);
+ this.deviceContext.Get()->HSSetShader(null, null, 0);
+ this.deviceContext.Get()->DSSetShader(null, null, 0);
+ this.deviceContext.Get()->CSSetShader(null, null, 0);
+
+ this.deviceContext.Get()->DrawIndexed(6, 0, 0);
+ }
+
+ [SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed")]
+ private void Setup()
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var rendererName = typeof(Renderer).FullName!.Replace('+', '.');
+
+ if (this.drawToPremulVertexShader.IsEmpty() || this.drawToPremulInputLayout.IsEmpty())
+ {
+ using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.vs.bin")!;
+ var array = ArrayPool.Shared.Rent((int)stream.Length);
+ stream.ReadExactly(array, 0, (int)stream.Length);
+
+ using var tempShader = default(ComPtr);
+ using var tempInputLayout = default(ComPtr);
+
+ fixed (byte* pArray = array)
+ fixed (void* pszPosition = "POSITION"u8)
+ fixed (void* pszTexCoord = "TEXCOORD"u8)
+ fixed (void* pszColor = "COLOR"u8)
+ {
+ this.device.Get()->CreateVertexShader(
+ pArray,
+ (nuint)stream.Length,
+ null,
+ tempShader.GetAddressOf()).ThrowOnError();
+
+ var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[]
+ {
+ new()
+ {
+ SemanticName = (sbyte*)pszPosition,
+ Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
+ AlignedByteOffset = uint.MaxValue,
+ },
+ new()
+ {
+ SemanticName = (sbyte*)pszTexCoord,
+ Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
+ AlignedByteOffset = uint.MaxValue,
+ },
+ new()
+ {
+ SemanticName = (sbyte*)pszColor,
+ Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
+ AlignedByteOffset = uint.MaxValue,
+ },
+ };
+ this.device.Get()->CreateInputLayout(
+ ied,
+ 3,
+ pArray,
+ (nuint)stream.Length,
+ tempInputLayout.GetAddressOf())
+ .ThrowOnError();
+ }
+
+ ArrayPool.Shared.Return(array);
+
+ tempShader.Swap(ref this.drawToPremulVertexShader);
+ tempInputLayout.Swap(ref this.drawToPremulInputLayout);
+ }
+
+ if (this.drawToPremulPixelShader.IsEmpty())
+ {
+ using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.ps.bin")!;
+ var array = ArrayPool.Shared.Rent((int)stream.Length);
+ stream.ReadExactly(array, 0, (int)stream.Length);
+
+ using var tmp = default(ComPtr);
+ fixed (byte* pArray = array)
+ {
+ this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf())
+ .ThrowOnError();
+ }
+
+ ArrayPool.Shared.Return(array);
+
+ tmp.Swap(ref this.drawToPremulPixelShader);
+ }
+
+ if (this.makeStraightVertexShader.IsEmpty() || this.makeStraightInputLayout.IsEmpty())
+ {
+ using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.vs.bin")!;
+ var array = ArrayPool.Shared.Rent((int)stream.Length);
+ stream.ReadExactly(array, 0, (int)stream.Length);
+
+ using var tempShader = default(ComPtr);
+ using var tempInputLayout = default(ComPtr);
+
+ fixed (byte* pArray = array)
+ fixed (void* pszPosition = "POSITION"u8)
+ {
+ this.device.Get()->CreateVertexShader(
+ pArray,
+ (nuint)stream.Length,
+ null,
+ tempShader.GetAddressOf()).ThrowOnError();
+
+ var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[]
+ {
+ new()
+ {
+ SemanticName = (sbyte*)pszPosition,
+ Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
+ AlignedByteOffset = uint.MaxValue,
+ },
+ };
+ this.device.Get()->CreateInputLayout(
+ ied,
+ 1,
+ pArray,
+ (nuint)stream.Length,
+ tempInputLayout.GetAddressOf())
+ .ThrowOnError();
+ }
+
+ ArrayPool.Shared.Return(array);
+
+ tempShader.Swap(ref this.makeStraightVertexShader);
+ tempInputLayout.Swap(ref this.makeStraightInputLayout);
+ }
+
+ if (this.makeStraightPixelShader.IsEmpty())
+ {
+ using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.ps.bin")!;
+ var array = ArrayPool.Shared.Rent((int)stream.Length);
+ stream.ReadExactly(array, 0, (int)stream.Length);
+
+ using var tmp = default(ComPtr);
+ fixed (byte* pArray = array)
+ {
+ this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf())
+ .ThrowOnError();
+ }
+
+ ArrayPool.Shared.Return(array);
+
+ tmp.Swap(ref this.makeStraightPixelShader);
+ }
+
+ if (this.makeStraightVertexBuffer.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var desc = new D3D11_BUFFER_DESC(
+ (uint)(sizeof(Vector2) * 4),
+ (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
+ D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
+ var data = stackalloc Vector2[] { new(-1, 1), new(-1, -1), new(1, 1), new(1, -1) };
+ var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
+ this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError();
+ tmp.Swap(ref this.makeStraightVertexBuffer);
+ }
+
+ if (this.makeStraightIndexBuffer.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var desc = new D3D11_BUFFER_DESC(
+ sizeof(ushort) * 6,
+ (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER,
+ D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
+ var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 };
+ var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
+ this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError();
+ tmp.Swap(ref this.makeStraightIndexBuffer);
+ }
+
+ if (this.drawToPremulVertexConstantBuffer.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var bufferDesc = new D3D11_BUFFER_DESC(
+ (uint)sizeof(TransformationBuffer),
+ (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER,
+ D3D11_USAGE.D3D11_USAGE_DYNAMIC,
+ (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE);
+ this.device.Get()->CreateBuffer(&bufferDesc, null, tmp.GetAddressOf()).ThrowOnError();
+
+ tmp.Swap(ref this.drawToPremulVertexConstantBuffer);
+ }
+
+ if (this.samplerState.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var samplerDesc = new D3D11_SAMPLER_DESC
+ {
+ Filter = D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR,
+ AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
+ AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
+ AddressW = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
+ MipLODBias = 0,
+ MaxAnisotropy = 0,
+ ComparisonFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS,
+ MinLOD = 0,
+ MaxLOD = 0,
+ };
+ this.device.Get()->CreateSamplerState(&samplerDesc, tmp.GetAddressOf()).ThrowOnError();
+
+ tmp.Swap(ref this.samplerState);
+ }
+
+ // Create the blending setup
+ if (this.blendState.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var blendStateDesc = new D3D11_BLEND_DESC
+ {
+ RenderTarget =
+ {
+ e0 =
+ {
+ BlendEnable = true,
+ SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA,
+ DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA,
+ BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
+ SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA,
+ DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE,
+ BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
+ RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL,
+ },
+ },
+ };
+ this.device.Get()->CreateBlendState(&blendStateDesc, tmp.GetAddressOf()).ThrowOnError();
+
+ tmp.Swap(ref this.blendState);
+ }
+
+ // Create the rasterizer state
+ if (this.rasterizerState.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var rasterizerDesc = new D3D11_RASTERIZER_DESC
+ {
+ FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID,
+ CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE,
+ ScissorEnable = true,
+ DepthClipEnable = true,
+ };
+ this.device.Get()->CreateRasterizerState(&rasterizerDesc, tmp.GetAddressOf()).ThrowOnError();
+
+ tmp.Swap(ref this.rasterizerState);
+ }
+
+ // Create the depth-stencil State
+ if (this.depthStencilState.IsEmpty())
+ {
+ using var tmp = default(ComPtr);
+ var dsDesc = new D3D11_DEPTH_STENCIL_DESC
+ {
+ DepthEnable = false,
+ StencilEnable = false,
+ };
+ this.device.Get()->CreateDepthStencilState(&dsDesc, tmp.GetAddressOf()).ThrowOnError();
+
+ tmp.Swap(ref this.depthStencilState);
+ }
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ this.device.Reset();
+ this.deviceContext.Reset();
+ this.drawToPremulVertexShader.Reset();
+ this.drawToPremulPixelShader.Reset();
+ this.drawToPremulInputLayout.Reset();
+ this.makeStraightVertexShader.Reset();
+ this.makeStraightPixelShader.Reset();
+ this.makeStraightInputLayout.Reset();
+ this.samplerState.Reset();
+ this.drawToPremulVertexConstantBuffer.Reset();
+ this.blendState.Reset();
+ this.rasterizerState.Reset();
+ this.depthStencilState.Reset();
+ this.drawToPremulVertexBuffer.Reset();
+ this.drawToPremulIndexBuffer.Reset();
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct TransformationBuffer
+ {
+ public Matrix4x4 View;
+ public Vector4 ColorMultiplier;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs
new file mode 100644
index 0000000000..342bfaa93d
--- /dev/null
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs
@@ -0,0 +1,136 @@
+using System.Diagnostics;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Textures.TextureWraps.Internal;
+
+///
+internal sealed unsafe partial class DrawListTextureWrap
+{
+ ///
+ public void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale)
+ {
+ ref var window = ref ImGuiWindow.FindWindowByName(windowName);
+ if (Unsafe.IsNullRef(ref window))
+ throw new ArgumentException("Window not found", nameof(windowName));
+
+ this.Size = window.Size;
+
+ var numDrawList = CountDrawList(ref window);
+ var drawLists = stackalloc ImDrawList*[numDrawList];
+ var drawData = new ImDrawData
+ {
+ Valid = 1,
+ CmdListsCount = numDrawList,
+ TotalIdxCount = 0,
+ TotalVtxCount = 0,
+ CmdLists = drawLists,
+ DisplayPos = window.Pos,
+ DisplaySize = window.Size,
+ FramebufferScale = scale,
+ };
+ AddWindowToDrawData(ref window, ref drawLists);
+ for (var i = 0; i < numDrawList; i++)
+ {
+ drawData.TotalVtxCount += drawData.CmdLists[i]->VtxBuffer.Size;
+ drawData.TotalIdxCount += drawData.CmdLists[i]->IdxBuffer.Size;
+ }
+
+ this.Draw(drawData);
+
+ return;
+
+ static bool IsWindowActiveAndVisible(scoped in ImGuiWindow window) =>
+ window.Active != 0 && window.Hidden == 0;
+
+ static void AddWindowToDrawData(scoped ref ImGuiWindow window, ref ImDrawList** wptr)
+ {
+ switch (window.DrawList.CmdBuffer.Size)
+ {
+ case 0:
+ case 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 &&
+ window.DrawList.CmdBuffer[0].UserCallback == 0:
+ break;
+ default:
+ *wptr++ = window.DrawList;
+ break;
+ }
+
+ for (var i = 0; i < window.DC.ChildWindows.Size; i++)
+ {
+ ref var child = ref *(ImGuiWindow*)window.DC.ChildWindows[i];
+ if (IsWindowActiveAndVisible(in child)) // Clipped children may have been marked not active
+ AddWindowToDrawData(ref child, ref wptr);
+ }
+ }
+
+ static int CountDrawList(scoped ref ImGuiWindow window)
+ {
+ var res = window.DrawList.CmdBuffer.Size switch
+ {
+ 0 => 0,
+ 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 &&
+ window.DrawList.CmdBuffer[0].UserCallback == 0 => 0,
+ _ => 1,
+ };
+ for (var i = 0; i < window.DC.ChildWindows.Size; i++)
+ res += CountDrawList(ref *(ImGuiWindow*)window.DC.ChildWindows[i]);
+ return res;
+ }
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 0x448)]
+ private struct ImGuiWindow
+ {
+ [FieldOffset(0x048)]
+ public Vector2 Pos;
+
+ [FieldOffset(0x050)]
+ public Vector2 Size;
+
+ [FieldOffset(0x0CB)]
+ public byte Active;
+
+ [FieldOffset(0x0D2)]
+ public byte Hidden;
+
+ [FieldOffset(0x118)]
+ public ImGuiWindowTempData DC;
+
+ [FieldOffset(0x2C0)]
+ public ImDrawListPtr DrawList;
+
+ private static nint pfnImGuiFindWindowByName;
+
+ public static ref ImGuiWindow FindWindowByName(ReadOnlySpan name)
+ {
+ var nb = Encoding.UTF8.GetByteCount(name);
+ var buf = stackalloc byte[nb + 1];
+ buf[Encoding.UTF8.GetBytes(name, new(buf, nb))] = 0;
+ if (pfnImGuiFindWindowByName == 0)
+ {
+ pfnImGuiFindWindowByName =
+ Process
+ .GetCurrentProcess()
+ .Modules
+ .Cast()
+ .First(x => x.ModuleName == "cimgui.dll")
+ .BaseAddress + 0x357F0;
+ }
+
+ return ref *((delegate* unmanaged)pfnImGuiFindWindowByName)(buf);
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 0xF0)]
+ public struct ImGuiWindowTempData
+ {
+ [FieldOffset(0x98)]
+ public ImVector ChildWindows;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
index a6584f9aae..5fa242abed 100644
--- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
+++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using Dalamud.Interface.ImGuiFileDialog;
@@ -48,6 +49,19 @@ public async Task ShowTextureSaveMenuAsync(
string name,
Task texture)
{
+ name = new StringBuilder(name)
+ .Replace('<', '_')
+ .Replace('>', '_')
+ .Replace(':', '_')
+ .Replace('"', '_')
+ .Replace('/', '_')
+ .Replace('\\', '_')
+ .Replace('|', '_')
+ .Replace('?', '_')
+ .Replace('*', '_')
+ .ToString();
+
+ var isCopy = false;
try
{
var initiatorScreenOffset = ImGui.GetMousePos();
@@ -55,11 +69,12 @@ public async Task ShowTextureSaveMenuAsync(
var textureManager = await Service.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}";
- BitmapCodecInfo encoder;
+ BitmapCodecInfo? encoder;
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
- var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var tcs = new TaskCompletionSource(
+ TaskCreationOptions.RunContinuationsAsynchronously);
Service.Get().Draw += DrawChoices;
encoder = await tcs.Task;
@@ -85,6 +100,8 @@ void DrawChoices()
return;
}
+ if (ImGui.Selectable("Copy"))
+ tcs.TrySetResult(null);
foreach (var encoder2 in encoders)
{
if (ImGui.Selectable(encoder2.Name))
@@ -106,8 +123,21 @@ void DrawChoices()
}
}
- string path;
+ if (encoder is null)
{
+ isCopy = true;
+ await textureManager.CopyToClipboardAsync(textureWrap, name, true);
+ }
+ else
+ {
+ 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;
+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
@@ -121,30 +151,23 @@ void DrawChoices()
else
tcs.SetResult(path2);
});
- path = await tcs.Task.ConfigureAwait(false);
- }
+ var path = await tcs.Task.ConfigureAwait(false);
+
+ await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
- 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);
-
- var notif = Service.Get().AddNotification(
- new()
+ var notif = Service.Get().AddNotification(
+ new()
+ {
+ Content = $"File saved to: {path}",
+ Title = initiatorName,
+ Type = NotificationType.Success,
+ });
+ notif.Click += n =>
{
- Content = $"File saved to: {path}",
- Title = initiatorName,
- Type = NotificationType.Success,
- });
- notif.Click += n =>
- {
- Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
- n.Notification.DismissNow();
- };
+ Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
+ n.Notification.DismissNow();
+ };
+ }
}
catch (Exception e)
{
@@ -155,7 +178,9 @@ void DrawChoices()
e,
$"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})");
Service.Get().AddNotification(
- $"Failed to save file: {e}",
+ isCopy
+ ? $"Failed to copy file: {e}"
+ : $"Failed to save file: {e}",
initiatorName,
NotificationType.Error);
}
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index d2a51235df..e1201d53f7 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -3,13 +3,17 @@
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal;
+using Dalamud.Interface.Textures.Internal;
+using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Internal;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -357,6 +361,7 @@ internal void DrawInternal(DalamudConfiguration? configuration)
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
(configuration?.EnablePluginUiAdditionalOptions ?? true) &&
flagsApplicableForTitleBarIcons;
+ var printWindow = false;
if (showAdditions)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
@@ -415,7 +420,10 @@ internal void DrawInternal(DalamudConfiguration? configuration)
if (!isAvailable)
ImGui.EndDisabled();
-
+
+ if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
+ printWindow = true;
+
ImGui.EndPopup();
}
@@ -474,6 +482,17 @@ internal void DrawInternal(DalamudConfiguration? configuration)
ImGui.End();
+ if (printWindow)
+ {
+ var tex = Service.Get().CreateDrawListTexture(
+ Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"));
+ tex.ResizeAndDrawWindow(this.WindowName, Vector2.One);
+ _ = Service.Get().ShowTextureSaveMenuAsync(
+ this.WindowName,
+ this.WindowName,
+ Task.FromResult(tex));
+ }
+
this.PostDraw();
if (hasNamespace)
diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs
index d914b1091e..2da8e1169a 100644
--- a/Dalamud/Plugin/Services/ITextureProvider.cs
+++ b/Dalamud/Plugin/Services/ITextureProvider.cs
@@ -5,11 +5,12 @@
using System.Threading;
using System.Threading.Tasks;
-using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
+using ImGuiNET;
+
using Lumina.Data.Files;
namespace Dalamud.Plugin.Services;
@@ -46,6 +47,14 @@ IDalamudTextureWrap CreateEmpty(
bool cpuWrite,
string? debugName = null);
+ /// Creates a texture that can be drawn from an or an .
+ ///
+ /// Name for debug display purposes.
+ /// A new draw list texture.
+ /// No new resource is allocated upfront; it will be done when is
+ /// set with positive values for both components.
+ IDrawListTextureWrap CreateDrawListTexture(string? debugName = null);
+
/// Creates a texture from the given existing texture, cropping and converting pixel format as needed.
///
/// The source texture wrap. The passed value may be disposed once this function returns,
@@ -170,6 +179,14 @@ Task CreateFromTexFileAsync(
string? debugName = null,
CancellationToken cancellationToken = default);
+ /// Creates a texture from clipboard.
+ /// Name for debug display purposes.
+ /// The cancellation token.
+ /// A representing the status of the operation.
+ Task CreateFromClipboardAsync(
+ string? debugName = null,
+ CancellationToken cancellationToken = default);
+
/// Gets the supported bitmap decoders.
/// The supported bitmap decoders.
///
@@ -192,6 +209,11 @@ Task CreateFromTexFileAsync(
/// Caching the returned object is not recommended. Performance benefit will be minimal.
///
ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup);
+
+ /// Gets a value indicating whether the current desktop clipboard contains an image that can be attempted
+ /// to read using .
+ /// true if it is the case.
+ bool HasClipboardImage();
/// Gets a shared texture corresponding to the given game resource icon specifier.
///
diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs
index 309be103a4..5549841ac6 100644
--- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs
+++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs
@@ -103,4 +103,17 @@ Task SaveToFileAsync(
IReadOnlyDictionary? props = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
+
+ /// Copies the texture to clipboard.
+ /// Texture wrap to copy.
+ /// Preferred file name.
+ /// Whether to leave non-disposed when the returned
+ /// completes.
+ /// The cancellation token.
+ /// A representing the status of the operation.
+ Task CopyToClipboardAsync(
+ IDalamudTextureWrap wrap,
+ string? preferredFileNameWithoutExtension = null,
+ bool leaveWrapOpen = false,
+ CancellationToken cancellationToken = default);
}
diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs
new file mode 100644
index 0000000000..07b6c00d6e
--- /dev/null
+++ b/Dalamud/Utility/ClipboardFormats.cs
@@ -0,0 +1,40 @@
+using System.Runtime.InteropServices;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Utility;
+
+/// Clipboard formats, looked up by their names.
+internal static class ClipboardFormats
+{
+ ///
+ public static uint FileContents { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILECONTENTS);
+
+ /// Gets the clipboard format corresponding to the PNG file format.
+ public static uint Png { get; } = ClipboardFormatFromName("PNG");
+
+ ///
+ public static uint FileDescriptorW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORW);
+
+ ///
+ public static uint FileDescriptorA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORA);
+
+ ///
+ public static uint FileNameW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEW);
+
+ ///
+ public static uint FileNameA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEA);
+
+ private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name)
+ {
+ uint cf;
+ fixed (void* p = name)
+ cf = RegisterClipboardFormatW((ushort*)p);
+ if (cf != 0)
+ return cf;
+ throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ??
+ new InvalidOperationException($"RegisterClipboardFormatW({name}) failed.");
+ }
+}
diff --git a/Dalamud/Utility/ThreadBoundTaskScheduler.cs b/Dalamud/Utility/ThreadBoundTaskScheduler.cs
index 4b6de29ff0..2930bd27f2 100644
--- a/Dalamud/Utility/ThreadBoundTaskScheduler.cs
+++ b/Dalamud/Utility/ThreadBoundTaskScheduler.cs
@@ -22,8 +22,14 @@ internal class ThreadBoundTaskScheduler : TaskScheduler
public ThreadBoundTaskScheduler(Thread? boundThread = null)
{
this.BoundThread = boundThread;
+ this.TaskQueued += static () => { };
}
+ ///
+ /// Event fired when a task has been posted.
+ ///
+ public event Action TaskQueued;
+
///
/// Gets or sets the thread this task scheduler is bound to.
///
@@ -57,6 +63,7 @@ protected override IEnumerable GetScheduledTasks()
///
protected override void QueueTask(Task task)
{
+ this.TaskQueued.Invoke();
this.scheduledTasks[task] = Scheduled;
}