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