From 4dce0c00e8ce568fa88c60860310fdf6690c4167 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 10 May 2025 05:47:42 +0900 Subject: [PATCH] Implement DrawListTextureWrap (#2036) * Implement DrawListTextureWrap * Fix unloading * minor fixes * Add CreateFromClipboardAsync --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Dalamud.csproj | 7 + .../Interface/Internal/StaThreadService.cs | 282 ++++++++ .../Windows/Data/Widgets/TexWidget.cs | 9 + .../Internal/TextureManager.Clipboard.cs | 498 +++++++++++++ .../Textures/Internal/TextureManager.Wic.cs | 16 +- .../Textures/Internal/TextureManager.cs | 28 +- .../Internal/TextureManagerPluginScoped.cs | 29 + .../TextureWraps/IDrawListTextureWrap.cs | 61 ++ .../Internal/DrawListTextureWrap.cs | 283 ++++++++ .../DeviceContextStateBackup.cs | 669 ++++++++++++++++++ .../DrawListTextureWrap/Renderer.Common.hlsl | 4 + .../Renderer.DrawToPremul.hlsl | 40 ++ .../Renderer.DrawToPremul.ps.bin | Bin 0 -> 16620 bytes .../Renderer.DrawToPremul.vs.bin | Bin 0 -> 16972 bytes .../Renderer.MakeStraight.hlsl | 22 + .../Renderer.MakeStraight.ps.bin | Bin 0 -> 14596 bytes .../Renderer.MakeStraight.vs.bin | Bin 0 -> 14360 bytes .../Internal/DrawListTextureWrap/Renderer.cs | 595 ++++++++++++++++ .../DrawListTextureWrap/WindowPrinter.cs | 136 ++++ .../Utility/Internal/DevTextureSaveMenu.cs | 75 +- Dalamud/Interface/Windowing/Window.cs | 19 + Dalamud/Plugin/Services/ITextureProvider.cs | 23 + .../Services/ITextureReadbackProvider.cs | 13 + Dalamud/Utility/ClipboardFormats.cs | 40 ++ Dalamud/Utility/ThreadBoundTaskScheduler.cs | 7 + 25 files changed, 2821 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Interface/Internal/StaThreadService.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs create mode 100644 Dalamud/Utility/ClipboardFormats.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 934d050a2..fd1c3ef64 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -114,6 +114,13 @@ + + + + + + + diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs new file mode 100644 index 000000000..87e003288 --- /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 07b2d01ff..ac48668fb 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -182,6 +182,15 @@ internal class TexWidget : IDataWindowWidget 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 000000000..8a510e967 --- /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 df84f9545..e7357a625 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -6,7 +6,6 @@ using System.IO; 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 sealed partial class TextureManager ReadOnlyMemory bytes, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -204,7 +203,7 @@ internal sealed partial class TextureManager string path, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -359,11 +358,18 @@ internal sealed partial class TextureManager /// 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 c9ee5d20e..8ac8e60ec 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -11,7 +11,9 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; 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 @@ internal sealed partial class TextureManager /// 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 @@ internal sealed partial class TextureManager 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 sealed partial class TextureManager /// 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 sealed partial class TextureManager /// 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 c62ad61b4..93600a263 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => + this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName); + /// public async Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, @@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped 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(); @@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped return shared; } + /// + public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage(); + /// public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) { @@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped 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 000000000..4fb5d1aca --- /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 000000000..4e82479b0 --- /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 000000000..55cf13881 --- /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 000000000..17b53ba6c --- /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 000000000..171d3e73b --- /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 0000000000000000000000000000000000000000..e3c68edf33f9766c259aa99b4ca284228754bec2 GIT binary patch literal 16620 zcmeHOU2Igx6+U;3gIR1?6Z2C*$v|ji6K8krxC!A;V6P2U4L05dT%H51 z?-~fDL5&(kX;rmq9xA1(ckoO9>QoS8XuX69~YsIzPM+8_65Z@>4?kKQ}~=9iy)zg82G>jy+0 zLAne0Utkf$%fQ1ik-q{5Gu=Hu2R#6gWG^3Tr>8Z@eHYomOj{din}7<~4BP>%2NJ#p ztZ{R*fAG2xUj?$Pg z7B6H^j12T=dvm@0Mepy>qAZ>_DbdP|2CG(Ut0Y>M3<3+oPUKrqd1a&6z91R(EmZL7j6=1w*>Q1{Vn|8q|m?gDa1-rXF zVSZ%_oM5|`4zW5^-Yl9Hxb7Yhtc!c9+D_OR-OMSlTggy~?dr2B8<aw6dlhtUOrjvsC{a(Qf!_WnJO4( zTfJ`V_zgDBl~*+R12)o6{y^lL(>1d7(U=_Ep~>~1YSK~CA^M?T^pME6g~v`8`&3A}>F$&++{2P^~5GdX1^NAbd)33D9{D@I#=#eS>rLUH-76~}j`;`2=SIi&n(Wqce5 zz&T4V+2f`)sdG*5^oXG~ZZzEzzYv-c=Q@UoKs2=lG2Wg?I+MZEux$!GiyKJf+ z!=x=@F>}1gE$N9M#3jQq%ie6}1YNX7hl1-xYi)iepqKeq`pDDI1UekAOV-nWMn}Ho zFKO*9O#R$%r@#88mp{<%c2m#0A>Kpsz2Ll%cbL4FW7&V3KK1<%amYme1t580coB?1 zHgoXSn|Uz*4Wul25!_n~u~CBF z`YM?k^F^6Zal=m->d`T)aSKEnOyewwKt7Rk;Q)}ZLAIUTYo475xM0)J6IlsY`HPY(%~m=^EMS@@iso9wW;w zqt2J*d#l{M#9TR)hvToR-0F~Ay)VZ)Ema8jzt=Y$_p|$dskSYu_y4y*$K7n?SEIae zaCS9__(1=VGtUQR8R)GH2vPc=S|IaGCVMzDSY1!&tEZY-dgo(Dv?Xi-e($k)RqDnj zv>|~-`*TP+eFJ>n&m)N-umINmSr7z_vxtlFu=t7@6LYLYuskfVK|b&9zb~OV+RU~b zard#tIL7%(|BEpCiR#S@WXW#;{}GqZZ^pgXJFL8I5g1obc64{nI|WLhs*-E`~LGujG0tlp>(FVG;T_&q)!^Qq--mn zI&JXpW1~egf5sz><6mTU>i%8gy0u!|N`&uLc@Dzw95~4){1NnHq^p1ouY!XR%54Yo zB$e{KbSK=uuW4C31$-S<&j44$UGl7#prd;9*}T8YBW*=`04ag@5W67D^3o@QJGj_*xht%S)4uAK8zo1}ZBx~kP>sUE&=Zg6p40fy2 zZVmOdMe6T>9mjH6%Pg7WwjLd7(U110`t{Vg%$Q;6-R5L5mq(&45o?pa)RE)opbx_# zA)wz692L`(9S@rFWonAT+%(Ky9Wk_lF~iUKQJT%(3fXj zpkJTUp4S$t&l^+9I^`H}!;>K)-_Zw}IyrJLmDf^7drsG(fkCu#7_u*fL_>e-df(KM zLjpL0=Gr6fNUAh-GCc!6^-Mr?Mt5ac23u;|kymM!Q4apBArH@dXvTXwPHu4PDV;2D z7YVC!#RpqfdUo`#b?bl?QhF6n$72c!m*T>>NLp55+v_p{IH$5OreS7V99n(&rnii)6+I==|K3M4eU zB=}th9{7NPGk~Yo1Bjb{dTKi#bbNLNVweu>^kNxZ!X)2QEDK8PjdRNT@ zS7L42@z-m{xeh(`+os)#P`vBX>A$}ClUvu5N}gp4*QvKoJhzag!~^;Sd7x|=u_2>? z&8lI=uw9)r3$b(gQhrA`T(Yfl*4FzblUC-!NyD=Bi(wt$Y>wOYqFKsukFW33)oyUV zzq~w!615{u_=-AoXS`G=_4Kvau75sdgH@DOvn_2* z+i+BSQZJ3zZW`8G#zd}U8&=d#wCoS3GLuE@^iy`mHXv4VvwpD>#fDB*8tZgV84g0) zEZfDh?H4Y$K{)JrW2!VVnaLHRS+h`@Hz2vOsTR*yo~UJ%aE#&l#!bg619d>3Ea#){ z2}!Fvj2-1~Y4Yo{B4$+N=7%ERo~@PUXKLiFtR}m^C-TQVHM09W_TxX&q_0Vn@87^1 zqq0?gf*$n1)T$oXnJZ-T^!I`a0`38wQst?6d^dc$MNV3kB7hdW$_V)3up9jJ;=ItN;@wyaVj*IE`tZ<^ zp0pR?EW>G|5`trH$cZY@L{A^B-r*%ZruZ$%0cn?#I zt9rcG)bIKDvmk8_S_Xn=Y^oF+MTPql=6lqy7=85=d%b=D#l;6!9NrTTSDo_nnAuV1 zh+eODZts5|jV4;c7kx8gxdEm~ccRuApemgh(XZH;s z$=Rn&>wIZAWm<+&G|a-pa59t6OqRzgw6B0w&v+(l45x~jLLoDnH%i0qA~FYYr?`;G z58wYR5qWvv@I=nO;HWOmsUGuGE1IjQF3qX7ob@YJJH=EzhDn>p%gkG?#at0AaamxS zMRzuHf-YI7eT(ZQ>uq*!K`+(c^g*PbTj=n4ePSd1=M3aq|D4d?!_+^c{pZCquYIIF z=%$``L%h>tJ#}9A3ewn42;2VK?Ah;rj4cDpF9KQShZj(wWRr*Q{2+_Me*!7XvH%{g z2RL?J(%H!Sbep=DF+rhPd9y!=p2mWfo;*)}lz(yHt)SRcX z&TGeQUGe!{fZP5R=NVS~kdF^lqWBFyev^+M@$q#Zzsbkn<>PPm@%ikl_M3hDR`3s^ zuOS$4Svu89)msBkW%6YMx3`M}+pR(W{rYrNQGCBXWgq2rpB>6R@^|_8v`@9;@4xaF z<^BCv{vw|dL-`$Imw z>wnXx?PZOK$QmHO0jkGWHEeqpXF-Gx52>e<48TU&y5SHgr)|jnUZ!0>o4Kv{tRGR| z(SZNF)UIE%L7g3`BPqG9cp)iIu|D$}?1+f7f0G@ug0-XuJO#SUok_AzST=8{MLEX} zk!wANjxj>%WP2Dv@4RwOXJmoS&7RI}p3ap%ol$Z3rb^!`Pv2SzML0({LH7#KZPM)c zh5dJZfW#rs5%?a7wuZgBOxHVU$Bi^{$-zJV>Rhj3!T2w^QGvoG^8mh)yZ?=RJw)o_r7Rdyg%vo^o7fO~m>PFsFZwO3pR2eAA+uu;C`?7uIeYuenlT(-!G#s)kI?%sk? zVs_U1wto3J;VJG>)yeMpgJaHj!&B^@W#4s<@rBiG&bjYeX?k;=G@a*vH4LKXIFxd#hKtAo9QSul{5FaIKTd-3*a zylsVsU8?)7D)oju9g0V)IojNUm(lj4yWUzV&Hg(k5p}oc_)|HRPNs8QSmR5xB!H*P z1B-qCxf{<+ygyes-&YtnC0@u(8YPLBtZe+8!GCWX&70ZtE@K@3&N*#nyZU>O7D$4WHo$%6{0H zmw|cjf%5(?3wje&o&QFWZ-p%14?G`cK;Hu7dV=TBd`GZt9vQKh+&`=bt^u<5^b9?) r7RdiWq=%jXHUT#PHv##s3E<&+VC%zeK4?AgK;VJE1Azw~QxE(Xn~Wqp literal 0 HcmV?d00001 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 000000000..b8423697a --- /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 0000000000000000000000000000000000000000..0b979f6b61f10856c82f9938e9bcf7c579d55b11 GIT binary patch literal 14596 zcmeHOU2GKB6+W{z#$Jr^QX5=o$xTI7L!A8~q@|8=3|^`&YnHA*9)G=1qy)CZIY9$F$*X%$6nAENZ7Qlui2D0zu+zi(!) zJtnM08i^l!4xByro^$Ux=bn3K=FYj}?8$@u4e8fT6t*|dz4`nrv3LIc+!aGa>c1(n z9<&GecVHu!J;33Z$ajIC07tR|gD*g)f&JKC)-5a;s8}eDWHTA^I)Dn;3ET;61`!95+z50}m#dYTYDJC~$DFcTo~WqKkzMNWk=zlLn>5EQNA;JdXNn~hGU;SS zE?v4blsoi9SD>R2Q~?RpS?85RHldI^aeDYzZYV!=>c{>*DhU z+kN_5k!Rl!nd{Hi3uW~E%BG;re|`NKOwne@Fco6-;Rnziv~7XR3Ce>pzyC+*Pd&i( zTIXPk&ua+uyhK^xKV<~n=IdAn+V@x=CCi}~s&j)wX~tZ~&JY!e1u*EB4<-7@>;hO-9+gIZcz{i*(k9#ThhgX$Z}bb4caTQ*09aF+gV2mYLO z0M~N_t0MxJdDDqt%|>9K$c;5S_-Nn@>u?)WKyU8?%8-Ca1CX)^R(AySzGQWeG8$d2 z2wY5NjN?CxO>j^6-sKaGfBp4OkNv7)^U#+sy#5Q_hcaRq{&oD3n^|FTRK8ClkbNYZ zJDeR6zRT4Dh1cF%vWS0wN8rjSkpnvod7xM)*Ge(@%e!^*t(lm-`W=y`dA#}gufg~C zAS3Top}q@IZ@fMda`9a!ts-MUn|OVP*=7tPFk z+RWatnULzhlAojs?1a4At6XPH?Oz<(x3$&e?D?l3^Ssi7UM5oeJsG`*f~2W>ReF&_ z?&EqG6g!18D&;rq^2x4cjk|SYj5PSqpiRRKBOlmixGdreIr-~|- z=R`buI;~OzdP-Dku&lCMyS%ntNi~EREjY3gn^^`F{6k zp;$TRnRb^gOw;37{3R^@ihpxU`c1I-r?L25z4$%TYx@qk z{k^m?hpB)2t^Jj!e)JDxg>4{OeHz%F`C(Gs_Xj9<0;&7`j~^|(h6a>91Eeg1dnW=s zcRDCNiB@j{$8!QLr~z*%fb$e%7yEX3^G=ectw;K9WygG`|gU&8IJ#-x1=|7oVT! zIUnv54e}u<+iNgx6~ew3BWHA4jr~ zb$I9l_^}KzRPW ziyitl+%*56(?rz$9*zLdd$u*D=5a~kO7yNPUR&}-AZY|EM1VJ}C140wEpV?B!Rm{^ z7Wt~T|DMCpjHPWkb}Yve>#?GDcqhWVOBhSvqpiL$tl1H*bkow$hBX_UH860_@wujq zm-pEyZO^wz+teo935GFt1?&P|0BYER0`~@C(E9|wtErpL3S)dz4|eL5vu*m1U-Mrq zP}VQ@*z@D_P?3jUPtLOq;nwK3)=O5cC{4xLg*iuTly zJyDjFZBARRq>x8SOx?nnp zZ3z0)wl)753!ncASck~{3G_u!FCT>R+qg^5fii!80cD;0A?*T>&jz;T&798#=EvHA zn}7^BdmsY; E14oeP;{X5v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1baeecdaef844bc9a92a43d68fae190205dc126d GIT binary patch literal 14360 zcmeHNPiz!b7=N=ZxZRer#kN#P@TH<@q3-OKBBYc;>9)|MK-t|0!jg8{ooH*V_q`WCb*S_EWh}Ho)umQLoSOFwBstuWjWenTAA<@W>^d@_G zazx3fChs)H#7X2~O6rv^~g2T7#!xgeAJ!QLDWBga^lvC=!-O;bNFVYJO&zT4wLS@4Ee z7B~o|c`$XL9m7bbI#QQWF7zN4m~hgT%Yk_En8qOW7BJS{A+#e>#&S*pz}=!C3}E4 zdjhKS;mNi@;ojuQ9;rXN`&r;}Bqp7BJ1|W9(EcwoXUhM*a`1!iuGQ;*y?0meQ{BRS z>Tf4@Ei4l3K3zo?i0_IgkHiyfpoq?#6|FAP(yvihaVqy-WuQ|hFhH`V5NTV6NJF+Q9#6uYIJTm44f9?#pnm78i-vuxFD z$j2J@g+eX|pRoGJ6isg!GIYy!n>F)s!_sWc(9uf7TN;rr2*2=)Ar3HANYy=Q4Wd~S zk2Y}<^7^cp@&AiyDHK!CwI?ankvQCy^5hg~*TOAxi0_{r*%=`p<vwEZiB~7<$N!u&70pyLe&v+Q_aXxWmHYtrlMs> z?8t-_8PxPeGLPh}T%ll0jVIWfq$LO*U$WrP4mtFY-tISFXo2Zl)3<0N_c0v=PPe8H zYxYUQ9JSI(!&KFA)zGu)xRO)G@|juMqhl=?R)*Aca$M1MWiY2&Y1h3pushYdl1nen z1Ieivd$l#4)$H?*XlqV%#uF{5HY?hi6Ky)4O`@G@5}kqb=ivOdySPPM7C8SHoZk}8 z?}%R8E8y1OqtX#zqUKa$u-1^n$*63WY%_;|bg5Bgin`l=*G zk*rd$FIR#nFD)o1TeP$Gs#5fSiM4q$#aDSk$>mjC>l6p-H}C*?RK8H2;13q)L3dxG zyrQ6-?$ZK@%-sX61Cq(Y(!HC%*8TYH^}W}AsIEB>`{o8?cThe6B#`iLz)86=y%f@9 zQ;c=WqfYFP@)QFk8>GE@k3#GE`{LgoKZw(gs1K{l0_9)FBZM+z~l!r zI13VxuMv`8av(u1+gfuFl-B+rb4~=!?Czc0Ay!vIXGiiotRkD7Wd(V`PyL2n{uaSs z;_+9qV3>Y;o6$A^>cgYAK;it0Gk^Jl`G53U_!5@O0(9TArA)p}zwMy6PxC&-%cauF zm+P-hD}RoB4HspBTK2rN|DFQ3G`B5x+KRPdQ62eAtI`6+mNI|-SLKv0Noktn^JOt7 z?z4eazgx+wM>k?6=tD0YNbfN(0R_||@r=lLRO}Z9kmqwpPp3A3llm7P1HFZGlAlhv z2MgrI!Cp6Xl^VSGRpfT@{sQbNVnk1*JEaDIw09v8Wkl%=;z?Z2&?XZxR%cvda6(y9 zbY`Z)QQS>a&0_=ay2if&S$wd1nQuT`?{}lA!c8a=Lykq!RXSlW9*VvH_ypa^y_!DS zqYoQQ)|D~UVlwWycoSP{t2t}6ez{-`+yv2f(9T*p1utf7ox?Sz6+JW zY9PJ0Cu7$D%YkJ4G9bk-Hvnl;Xd*~9P14iAO+X(WzXdiwev!UJ-vYh`d<*y%SOFIJ E4`unISO5S3 literal 0 HcmV?d00001 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 000000000..cc6cfd000 --- /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 000000000..342bfaa93 --- /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 a6584f9aa..5fa242abe 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; 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 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService 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 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService 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 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService return; } + if (ImGui.Selectable("Copy")) + tcs.TrySetResult(null); foreach (var encoder2 in encoders) { if (ImGui.Selectable(encoder2.Name)) @@ -106,8 +123,21 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService } } - 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 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService else tcs.SetResult(path2); }); - path = await tcs.Task.ConfigureAwait(false); - } + var path = await tcs.Task.ConfigureAwait(false); - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + 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 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService 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 7779100b0..8dc517cb2 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using CheapLoc; @@ -11,7 +12,10 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; 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.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; @@ -408,6 +412,7 @@ public abstract class Window var showAdditions = (this.AllowPinning || this.AllowClickthrough) && internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) && flagsApplicableForTitleBarIcons; + var printWindow = false; if (showAdditions) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); @@ -482,6 +487,9 @@ public abstract class Window if (!isAvailable) ImGui.EndDisabled(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) + printWindow = true; + ImGui.EndPopup(); } @@ -540,6 +548,17 @@ public abstract class Window 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(); this.PostHandlePreset(persistence); diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 201e2b803..3f9ae99df 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -9,6 +9,8 @@ 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; @@ -45,6 +47,14 @@ public interface ITextureProvider 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, @@ -169,6 +179,14 @@ public interface ITextureProvider 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 +210,11 @@ public interface ITextureProvider /// 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. /// /// This function does not throw exceptions. diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs index b41ded41f..3d2894355 100644 --- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -106,4 +106,17 @@ public interface ITextureReadbackProvider 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 000000000..07b6c00d6 --- /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 4b6de29ff..2930bd27f 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 @@ internal class ThreadBoundTaskScheduler : TaskScheduler /// protected override void QueueTask(Task task) { + this.TaskQueued.Invoke(); this.scheduledTasks[task] = Scheduled; }