Implement DrawListTextureWrap (#2036)

* Implement DrawListTextureWrap

* Fix unloading

* minor fixes

* Add CreateFromClipboardAsync

---------

Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
This commit is contained in:
srkizer 2025-05-10 05:47:42 +09:00 committed by GitHub
parent a12c63d6a2
commit 4dce0c00e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2821 additions and 35 deletions

View file

@ -114,6 +114,13 @@
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" /> <EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.DrawToPremul.ps.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.DrawToPremul.vs.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.MakeStraight.ps.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.MakeStraight.vs.bin" />
</ItemGroup>
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles"> <Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
<ItemGroup> <ItemGroup>
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" /> <ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />

View file

@ -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;
/// <summary>Dedicated thread for OLE operations, and possibly more native thread-serialized operations.</summary>
[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<HWND> 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;
}
}
/// <summary>Gets all the available clipboard formats.</summary>
public IReadOnlySet<uint> AvailableClipboardFormats { get; private set; } = ImmutableSortedSet<uint>.Empty;
/// <summary>Places a pointer to a specific data object onto the clipboard. This makes the data object accessible
/// to the <see cref="OleGetClipboard(IDataObject**)"/> function.</summary>
/// <param name="pdo">Pointer to the <see cref="IDataObject"/> 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.
/// </param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleSetClipboard(IDataObject* pdo);
/// <inheritdoc cref="OleSetClipboard(IDataObject*)"/>
public static unsafe void OleSetClipboard(ComPtr<IDataObject> pdo) =>
Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get()));
/// <summary>Retrieves a data object that you can use to access the contents of the clipboard.</summary>
/// <param name="pdo">Address of <see cref="IDataObject"/> pointer variable that receives the interface pointer to
/// the clipboard data object.</param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleGetClipboard(IDataObject** pdo);
/// <inheritdoc cref="OleGetClipboard(IDataObject**)"/>
public static unsafe ComPtr<IDataObject> OleGetClipboard()
{
var pdo = default(ComPtr<IDataObject>);
Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf()));
return pdo;
}
/// <summary>Calls the appropriate method or function to release the specified storage medium.</summary>
/// <param name="stgm">Address of <see cref="STGMEDIUM"/> to release.</param>
[LibraryImport("ole32.dll")]
public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm);
/// <inheritdoc cref="ReleaseStgMedium(STGMEDIUM*)"/>
public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm)
{
fixed (STGMEDIUM* pstgm = &stgm)
ReleaseStgMedium(pstgm);
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cancellationTokenSource.Cancel();
if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully)
SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0);
this.thread.Join();
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="action">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task"/> representating the state of the operation.</returns>
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);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<T> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task Run(Func<Task> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<Task<T>> 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<uint>.Empty;
return;
}
var formats = new SortedSet<uint>();
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);
}
}
}

View file

@ -182,6 +182,15 @@ internal class TexWidget : IDataWindowWidget
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); 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))) if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon)))
{ {
ImGui.PushID(nameof(this.DrawGetFromGameIcon)); ImGui.PushID(nameof(this.DrawGetFromGameIcon));

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
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<IDataObject>);
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<string, object> { ["InterlaceOption"] = true },
true,
true,
cancellationToken);
unsafe
{
using var ims = default(ComPtr<IStream>);
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<FILEGROUPDESCRIPTORA>(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<FILEGROUPDESCRIPTORW>(new(ref fgdw)),
});
}
}
}
{
ms.SetLength(ms.Position = 0);
await this.SaveToStreamAsync(
wrap,
GUID.GUID_ContainerFormatBmp,
ms,
new Dictionary<string, object> { ["EnableV5Header32bppBGRA"] = false },
true,
true,
cancellationToken);
AddToDataObject(
pdo,
CF.CF_DIB,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<byte>(
ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf<BITMAPFILEHEADER>()..]),
});
}
if (hasAlphaChannel)
{
ms.SetLength(ms.Position = 0);
await this.SaveToStreamAsync(
wrap,
GUID.GUID_ContainerFormatBmp,
ms,
new Dictionary<string, object> { ["EnableV5Header32bppBGRA"] = true },
true,
true,
cancellationToken);
AddToDataObject(
pdo,
CF.CF_DIBV5,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<byte>(
ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf<BITMAPFILEHEADER>()..]),
});
}
var omts = await Service<StaThreadService>.GetAsync();
await omts.Run(() => StaThreadService.OleSetClipboard(pdo), cancellationToken);
return;
static unsafe void AddToDataObject(ComPtr<IDataObject> 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<T>(ReadOnlySpan<T> 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;
}
}
/// <inheritdoc/>
public bool HasClipboardImage()
{
var acf = Service<StaThreadService>.Get().AvailableClipboardFormats;
return acf.Contains(CF.CF_DIBV5)
|| acf.Contains(CF.CF_DIB)
|| acf.Contains(ClipboardFormats.Png)
|| acf.Contains(ClipboardFormats.FileContents);
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromClipboardAsync(
string? debugName = null,
CancellationToken cancellationToken = default)
{
var omts = await Service<StaThreadService>.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<IDalamudTextureWrap> 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<byte>(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<IDalamudTextureWrap>(new NotSupportedException());
}
ref var bfh = ref Unsafe.As<byte, BITMAPFILEHEADER>(ref ms.GetBuffer()[0]);
bfh.bfType = 0x4D42;
bfh.bfSize = (uint)ms.Length;
ref var bih = ref Unsafe.As<byte, BITMAPINFOHEADER>(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<IDalamudTextureWrap> 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<IStream>(stgm.pstm);
return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
}
default:
return Task.FromException<IDalamudTextureWrap>(new NotSupportedException());
}
}
static unsafe bool TryGetClipboardDataAs(
ComPtr<IDataObject> 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.");
}
}
}

View file

@ -6,7 +6,6 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -173,7 +172,7 @@ internal sealed partial class TextureManager
ReadOnlyMemory<byte> bytes, ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try try
@ -204,7 +203,7 @@ internal sealed partial class TextureManager
string path, string path,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try try
@ -359,11 +358,18 @@ internal sealed partial class TextureManager
/// <param name="handle">An instance of <see cref="MemoryHandle"/>.</param> /// <param name="handle">An instance of <see cref="MemoryHandle"/>.</param>
/// <param name="length">The number of bytes in the memory.</param> /// <param name="length">The number of bytes in the memory.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns> /// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(MemoryHandle handle, int length) public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(MemoryHandle handle, int length) =>
this.CreateIStreamViewOfMemory((byte*)handle.Pointer, length);
/// <summary>Creates a new instance of <see cref="IStream"/> from a fixed memory allocation.</summary>
/// <param name="address">Address of the data.</param>
/// <param name="length">The number of bytes in the memory.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(void* address, int length)
{ {
using var wicStream = default(ComPtr<IWICStream>); using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); 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<IStream>); var res = default(ComPtr<IStream>);
wicStream.As(ref res).ThrowOnError(); wicStream.As(ref res).ThrowOnError();

View file

@ -11,7 +11,9 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom; using Dalamud.Utility.TerraFxCom;
@ -48,10 +50,11 @@ internal sealed partial class TextureManager
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get(); private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly CancellationTokenSource disposeCts = new();
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager; private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager; private WicManager? wicManager;
private bool disposing;
private ComPtr<ID3D11Device> device; private ComPtr<ID3D11Device> device;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
@ -104,10 +107,10 @@ internal sealed partial class TextureManager
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
if (this.disposing) if (this.disposeCts.IsCancellationRequested)
return; return;
this.disposing = true; this.disposeCts.Cancel();
Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose();
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
@ -269,6 +272,21 @@ internal sealed partial class TextureManager
return wrap; return wrap;
} }
/// <inheritdoc/>
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName);
/// <summary><inheritdoc cref="CreateDrawListTexture(string?)" path="/summary"/></summary>
/// <param name="plugin">Plugin that created the draw list.</param>
/// <param name="debugName"><inheritdoc cref="CreateDrawListTexture(string?)" path="/param[name=debugName]"/></param>
/// <returns><inheritdoc cref="CreateDrawListTexture(string?)" path="/returns"/></returns>
public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) =>
new DrawListTextureWrap(
new(this.device),
this,
Service<DalamudAssetManager>.Get().Empty4X4,
plugin,
debugName ?? $"{nameof(this.CreateDrawListTexture)}");
/// <inheritdoc/> /// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
@ -330,7 +348,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var buffer = file.TextureBuffer; var buffer = file.TextureBuffer;
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
@ -354,7 +372,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes) internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile."); throw new InvalidDataException("The file is not a TexFile.");

View file

@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped
return textureWrap; return textureWrap;
} }
/// <inheritdoc/>
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) =>
this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName);
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync( public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap, IDalamudTextureWrap wrap,
@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped
return textureWrap; return textureWrap;
} }
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> 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;
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() => public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped
return shared; return shared;
} }
/// <inheritdoc/>
public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage();
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture)
{ {
@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped
cancellationToken); cancellationToken);
} }
/// <inheritdoc/>
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) => private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) =>
this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); this.InterceptTexDataLoad?.Invoke(path, ref replacementPath);
} }

View file

@ -0,0 +1,61 @@
using System.Numerics;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using ImGuiNET;
namespace Dalamud.Interface.Textures.TextureWraps;
/// <summary>A texture wrap that can be drawn using ImGui draw data.</summary>
public interface IDrawListTextureWrap : IDalamudTextureWrap
{
/// <summary>Gets or sets the width of the texture.</summary>
/// <remarks>If <see cref="Height"/> is to be set together, set use <see cref="Size"/> instead.</remarks>
new int Width { get; set; }
/// <summary>Gets or sets the width of the texture.</summary>
/// <remarks>If <see cref="Width"/> is to be set together, set use <see cref="Size"/> instead.</remarks>
new int Height { get; set; }
/// <summary>Gets or sets the size of the texture.</summary>
/// <remarks>Components will be rounded up.</remarks>
new Vector2 Size { get; set; }
/// <inheritdoc/>
int IDalamudTextureWrap.Width => this.Width;
/// <inheritdoc/>
int IDalamudTextureWrap.Height => this.Height;
/// <inheritdoc/>
Vector2 IDalamudTextureWrap.Size => this.Size;
/// <summary>Gets or sets the color to use when clearing this texture.</summary>
/// <value>Color in RGBA. Defaults to <see cref="Vector4.Zero"/>, which is full transparency.</value>
Vector4 ClearColor { get; set; }
/// <summary>Draws a draw list to this texture.</summary>
/// <param name="drawListPtr">Draw list to draw from.</param>
/// <param name="displayPos">Left-top coordinates of the draw commands in the draw list.</param>
/// <param name="scale">Scale to apply to all draw commands in the draw list.</param>
/// <remarks>This function can be called only from the main thread.</remarks>
void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale);
/// <inheritdoc cref="DrawListTextureWrap.Draw(ImGuiNET.ImDrawDataPtr)"/>
void Draw(scoped in ImDrawData drawData);
/// <summary>Draws from a draw data to this texture.</summary>
/// <param name="drawData">Draw data to draw.</param>
/// <remarks><ul>
/// <li>Texture size will be kept as specified in <see cref="Size"/>. <see cref="ImDrawData.DisplaySize"/> will be
/// used only as shader parameters.</li>
/// <li>This function can be called only from the main thread.</li>
/// </ul></remarks>
void Draw(ImDrawDataPtr drawData);
/// <summary>Resizes this texture and draws an ImGui window.</summary>
/// <param name="windowName">Name and ID of the window to draw. Use the value that goes into
/// <see cref="ImGui.Begin(string)"/>.</param>
/// <param name="scale">Scale to apply to all draw commands in the draw list.</param>
void ResizeAndDrawWindow(ReadOnlySpan<char> windowName, Vector2 scale);
}

View file

@ -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;
/// <inheritdoc cref="IDrawListTextureWrap"/>
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<ID3D11Device> device;
private ComPtr<ID3D11DeviceContext> deviceContext;
private ComPtr<ID3D11Texture2D> tex;
private ComPtr<ID3D11ShaderResourceView> srv;
private ComPtr<ID3D11RenderTargetView> rtv;
private ComPtr<ID3D11UnorderedAccessView> uav;
private int width;
private int height;
private DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM;
/// <summary>Initializes a new instance of the <see cref="DrawListTextureWrap"/> class.</summary>
/// <param name="device">Pointer to a D3D11 device. Ownership is taken.</param>
/// <param name="textureManager">Instance of the <see cref="ITextureProvider"/> class.</param>
/// <param name="emptyTexture">Texture to use, if <see cref="Width"/> or <see cref="Height"/> is <c>0</c>.</param>
/// <param name="plugin">Plugin that holds responsible for this texture.</param>
/// <param name="debugName">Name for debug display purposes.</param>
public DrawListTextureWrap(
ComPtr<ID3D11Device> 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);
}
/// <summary>Finalizes an instance of the <see cref="DrawListTextureWrap"/> class.</summary>
~DrawListTextureWrap() => this.RealDispose();
/// <inheritdoc/>
public nint ImGuiHandle => (nint)this.srv.Get();
/// <inheritdoc cref="IDrawListTextureWrap.Width"/>
public int Width
{
get => this.width;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value));
this.Resize(value, this.height, this.format);
}
}
/// <inheritdoc cref="IDrawListTextureWrap.Height"/>
public int Height
{
get => this.height;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value));
this.Resize(this.width, value, this.format).ThrowOnError();
}
}
/// <inheritdoc cref="IDrawListTextureWrap.Size"/>
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();
}
}
/// <inheritdoc/>
public Vector4 ClearColor { get; set; }
/// <summary>Gets or sets the <see cref="DXGI_FORMAT"/>.</summary>
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();
}
}
/// <inheritdoc/>
public void Dispose()
{
if (Service<InterfaceManager>.GetNullable() is { } im)
im.EnqueueDeferredDispose(this);
else
this.RealDispose();
}
/// <inheritdoc/>
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
}
/// <inheritdoc/>
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,
});
/// <inheritdoc/>
public void Draw(scoped in ImDrawData drawData)
{
fixed (ImDrawData* pDrawData = &drawData)
this.Draw(new(pDrawData));
}
/// <inheritdoc/>
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<Renderer>.Get().RenderDrawData(this.rtv.Get(), drawData);
Service<Renderer>.Get().MakeStraight(this.uav.Get());
}
}
/// <summary>Resizes the texture.</summary>
/// <param name="newWidth">New texture width.</param>
/// <param name="newHeight">New texture height.</param>
/// <param name="newFormat">New format.</param>
/// <returns><see cref="S.S_OK"/> if the texture has been resized, <see cref="S.S_FALSE"/> if the texture has not
/// been resized, or a value with <see cref="HRESULT.FAILED"/> that evaluates to <see langword="true"/>.</returns>
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<ID3D11Texture2D>);
using var tmpsrv = default(ComPtr<ID3D11ShaderResourceView>);
using var tmprtv = default(ComPtr<ID3D11RenderTargetView>);
using var tmpuav = default(ComPtr<ID3D11UnorderedAccessView>);
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;
}
}

View file

@ -0,0 +1,669 @@
using System.Runtime.InteropServices;
using ImGuiNET;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Textures.TextureWraps.Internal;
/// <inheritdoc cref="IDrawListTextureWrap"/>
internal sealed unsafe partial class DrawListTextureWrap
{
/// <summary>Captures states of a <see cref="ID3D11DeviceContext"/>.</summary>
// 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;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceContextStateBackup"/> struct,
/// by capturing all states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
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);
}
/// <inheritdoc/>
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();
}
/// <summary>
/// Captures Input Assembler states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct InputAssemblerState : IDisposable
{
private const int BufferCount = D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11InputLayout> layout;
private ComPtr<ID3D11Buffer> 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];
/// <summary>
/// Creates a new instance of <see cref="InputAssemblerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
}
}
}
/// <summary>
/// Captures Rasterizer states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct RasterizerState : IDisposable
{
private const int Count = D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX;
private ComPtr<ID3D11DeviceContext> context;
private ComPtr<ID3D11RasterizerState> state;
private fixed byte viewports[24 * Count];
private fixed ulong scissorRects[16 * Count];
/// <summary>
/// Creates a new instance of <see cref="RasterizerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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();
}
}
}
/// <summary>
/// Captures Output Merger states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11BlendState> blendState;
private fixed float blendFactor[4];
private uint sampleMask;
private uint stencilRef;
private ComPtr<ID3D11DepthStencilState> depthStencilState;
private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount]
private ComPtr<ID3D11DepthStencilView> dsv;
private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount]
private int uavCount;
/// <summary>
/// Creates a new instance of <see cref="OutputMergerState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11RenderTargetView>>(pThis->rtvs, RtvCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11UnorderedAccessView>>(pThis->uavs, this.uavCount))
b.Dispose();
}
}
}
/// <summary>
/// Captures Vertex Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11VertexShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="VertexShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Hull Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11HullShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="HullShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Domain Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11DomainShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="DomainShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Geometry Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11GeometryShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="GeometryShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Pixel Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11PixelShader> shader;
private fixed ulong insts[ClassInstanceCount];
private fixed ulong buffers[BufferCount];
private fixed ulong samplers[SamplerCount];
private fixed ulong resources[ResourceCount];
private uint instCount;
/// <summary>
/// Creates a new instance of <see cref="PixelShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
/// <summary>
/// Captures Compute Shader states of a <see cref="ID3D11DeviceContext"/>.
/// </summary>
[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<ID3D11DeviceContext> context;
private ComPtr<ID3D11ComputeShader> 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;
/// <summary>
/// Creates a new instance of <see cref="ComputeShaderState"/> from <paramref name="ctx"/>.
/// </summary>
/// <param name="featureLevel">The feature level.</param>
/// <param name="ctx">The device context.</param>
/// <returns>The captured state.</returns>
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;
}
/// <inheritdoc/>
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<ComPtr<ID3D11Buffer>>(pThis->buffers, BufferCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11SamplerState>>(pThis->samplers, SamplerCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ShaderResourceView>>(pThis->resources, ResourceCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11ClassInstance>>(pThis->insts, (int)pThis->instCount))
b.Dispose();
foreach (ref var b in new Span<ComPtr<ID3D11UnorderedAccessView>>(pThis->uavs, this.uavCount))
b.Dispose();
pThis->context.Dispose();
pThis->shader.Dispose();
}
}
}
}
}

View file

@ -0,0 +1,4 @@
cbuffer TransformationBuffer : register(b0) {
float4x4 g_view;
float4 g_colorMultiplier;
}

View file

@ -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<float4> 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
*/

View file

@ -0,0 +1,22 @@
RWTexture2D<unorm float4> 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
*/

View file

@ -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;
/// <inheritdoc cref="IDrawListTextureWrap"/>
internal sealed unsafe partial class DrawListTextureWrap
{
/// <summary>The renderer.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class Renderer : IInternalDisposableService
{
private ComPtr<ID3D11Device> device;
private ComPtr<ID3D11DeviceContext> deviceContext;
private ComPtr<ID3D11VertexShader> drawToPremulVertexShader;
private ComPtr<ID3D11PixelShader> drawToPremulPixelShader;
private ComPtr<ID3D11InputLayout> drawToPremulInputLayout;
private ComPtr<ID3D11Buffer> drawToPremulVertexBuffer;
private ComPtr<ID3D11Buffer> drawToPremulVertexConstantBuffer;
private ComPtr<ID3D11Buffer> drawToPremulIndexBuffer;
private ComPtr<ID3D11VertexShader> makeStraightVertexShader;
private ComPtr<ID3D11PixelShader> makeStraightPixelShader;
private ComPtr<ID3D11InputLayout> makeStraightInputLayout;
private ComPtr<ID3D11Buffer> makeStraightVertexBuffer;
private ComPtr<ID3D11Buffer> makeStraightIndexBuffer;
private ComPtr<ID3D11SamplerState> samplerState;
private ComPtr<ID3D11BlendState> blendState;
private ComPtr<ID3D11RasterizerState> rasterizerState;
private ComPtr<ID3D11DepthStencilState> 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;
}
}
/// <summary>Finalizes an instance of the <see cref="Renderer"/> class.</summary>
~Renderer() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
public void DisposeService() => this.ReleaseUnmanagedResources();
/// <summary>Renders draw data.</summary>
/// <param name="prtv">The render target.</param>
/// <param name="drawData">Pointer to the draw data.</param>
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<ImDrawListPtr>(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<ImDrawVert>(vertexData.pData, this.vertexBufferSize);
var targetIndices = new Span<ushort>(indexData.pData, this.indexBufferSize);
foreach (ref var cmdList in cmdLists)
{
var vertices = new ImVectorWrapper<ImDrawVert>(&cmdList.NativePtr->VtxBuffer);
var indices = new ImVectorWrapper<ushort>(&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<ImDrawCmd>(&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;
}
}
/// <summary>Renders draw data.</summary>
/// <param name="puav">The pointer to a Texture2D UAV to make straight.</param>
public void MakeStraight(ID3D11UnorderedAccessView* puav)
{
ThreadSafety.AssertMainThread();
D3D11_TEXTURE2D_DESC texDesc;
using (var texRes = default(ComPtr<ID3D11Resource>))
{
puav->GetResource(texRes.GetAddressOf());
using var tex = default(ComPtr<ID3D11Texture2D>);
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<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
using var tempShader = default(ComPtr<ID3D11VertexShader>);
using var tempInputLayout = default(ComPtr<ID3D11InputLayout>);
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<byte>.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<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
using var tmp = default(ComPtr<ID3D11PixelShader>);
fixed (byte* pArray = array)
{
this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf())
.ThrowOnError();
}
ArrayPool<byte>.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<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
using var tempShader = default(ComPtr<ID3D11VertexShader>);
using var tempInputLayout = default(ComPtr<ID3D11InputLayout>);
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<byte>.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<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
using var tmp = default(ComPtr<ID3D11PixelShader>);
fixed (byte* pArray = array)
{
this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf())
.ThrowOnError();
}
ArrayPool<byte>.Shared.Return(array);
tmp.Swap(ref this.makeStraightPixelShader);
}
if (this.makeStraightVertexBuffer.IsEmpty())
{
using var tmp = default(ComPtr<ID3D11Buffer>);
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<ID3D11Buffer>);
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<ID3D11Buffer>);
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<ID3D11SamplerState>);
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<ID3D11BlendState>);
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<ID3D11RasterizerState>);
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<ID3D11DepthStencilState>);
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;
}
}
}

View file

@ -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;
/// <inheritdoc cref="IDrawListTextureWrap"/>
internal sealed unsafe partial class DrawListTextureWrap
{
/// <inheritdoc/>
public void ResizeAndDrawWindow(ReadOnlySpan<char> 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<char> 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<ProcessModule>()
.First(x => x.ModuleName == "cimgui.dll")
.BaseAddress + 0x357F0;
}
return ref *((delegate* unmanaged<byte*, ImGuiWindow*>)pfnImGuiFindWindowByName)(buf);
}
[StructLayout(LayoutKind.Explicit, Size = 0xF0)]
public struct ImGuiWindowTempData
{
[FieldOffset(0x98)]
public ImVector<nint> ChildWindows;
}
}
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
@ -48,6 +49,19 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
string name, string name,
Task<IDalamudTextureWrap> texture) Task<IDalamudTextureWrap> texture)
{ {
name = new StringBuilder(name)
.Replace('<', '_')
.Replace('>', '_')
.Replace(':', '_')
.Replace('"', '_')
.Replace('/', '_')
.Replace('\\', '_')
.Replace('|', '_')
.Replace('?', '_')
.Replace('*', '_')
.ToString();
var isCopy = false;
try try
{ {
var initiatorScreenOffset = ImGui.GetMousePos(); var initiatorScreenOffset = ImGui.GetMousePos();
@ -55,11 +69,12 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
var textureManager = await Service<TextureManager>.GetAsync(); var textureManager = await Service<TextureManager>.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}";
BitmapCodecInfo encoder; BitmapCodecInfo? encoder;
{ {
var first = true; var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<BitmapCodecInfo?>(
TaskCreationOptions.RunContinuationsAsynchronously);
Service<InterfaceManager>.Get().Draw += DrawChoices; Service<InterfaceManager>.Get().Draw += DrawChoices;
encoder = await tcs.Task; encoder = await tcs.Task;
@ -85,6 +100,8 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
return; return;
} }
if (ImGui.Selectable("Copy"))
tcs.TrySetResult(null);
foreach (var encoder2 in encoders) foreach (var encoder2 in encoders)
{ {
if (ImGui.Selectable(encoder2.Name)) if (ImGui.Selectable(encoder2.Name))
@ -106,8 +123,21 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
} }
} }
string path; if (encoder is null)
{ {
isCopy = true;
await textureManager.CopyToClipboardAsync(textureWrap, name, true);
}
else
{
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog( this.fileDialogManager.SaveFileDialog(
"Save texture...", "Save texture...",
@ -121,30 +151,23 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
else else
tcs.SetResult(path2); tcs.SetResult(path2);
}); });
path = await tcs.Task.ConfigureAwait(false); var path = await tcs.Task.ConfigureAwait(false);
}
var props = new Dictionary<string, object>(); await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
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<NotificationManager>.Get().AddNotification( var notif = Service<NotificationManager>.Get().AddNotification(
new() new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
{ {
Content = $"File saved to: {path}", Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
Title = initiatorName, n.Notification.DismissNow();
Type = NotificationType.Success, };
}); }
notif.Click += n =>
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
n.Notification.DismissNow();
};
} }
catch (Exception e) catch (Exception e)
{ {
@ -155,7 +178,9 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
e, e,
$"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})");
Service<NotificationManager>.Get().AddNotification( Service<NotificationManager>.Get().AddNotification(
$"Failed to save file: {e}", isCopy
? $"Failed to copy file: {e}"
: $"Failed to save file: {e}",
initiatorName, initiatorName,
NotificationType.Error); NotificationType.Error);
} }

View file

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
@ -11,7 +12,10 @@ using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Windowing.Persistence; using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -408,6 +412,7 @@ public abstract class Window
var showAdditions = (this.AllowPinning || this.AllowClickthrough) && var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) && internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
flagsApplicableForTitleBarIcons; flagsApplicableForTitleBarIcons;
var printWindow = false;
if (showAdditions) if (showAdditions)
{ {
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
@ -482,6 +487,9 @@ public abstract class Window
if (!isAvailable) if (!isAvailable)
ImGui.EndDisabled(); ImGui.EndDisabled();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
ImGui.EndPopup(); ImGui.EndPopup();
} }
@ -540,6 +548,17 @@ public abstract class Window
ImGui.End(); ImGui.End();
if (printWindow)
{
var tex = Service<TextureManager>.Get().CreateDrawListTexture(
Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"));
tex.ResizeAndDrawWindow(this.WindowName, Vector2.One);
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
this.WindowName,
this.WindowName,
Task.FromResult<IDalamudTextureWrap>(tex));
}
this.PostDraw(); this.PostDraw();
this.PostHandlePreset(persistence); this.PostHandlePreset(persistence);

View file

@ -9,6 +9,8 @@ using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures; using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using ImGuiNET;
using Lumina.Data.Files; using Lumina.Data.Files;
namespace Dalamud.Plugin.Services; namespace Dalamud.Plugin.Services;
@ -45,6 +47,14 @@ public interface ITextureProvider
bool cpuWrite, bool cpuWrite,
string? debugName = null); string? debugName = null);
/// <summary>Creates a texture that can be drawn from an <see cref="ImDrawList"/> or an <see cref="ImDrawData"/>.
/// </summary>
/// <param name="debugName">Name for debug display purposes.</param>
/// <returns>A new draw list texture.</returns>
/// <remarks>No new resource is allocated upfront; it will be done when <see cref="IDrawListTextureWrap.Size"/> is
/// set with positive values for both components.</remarks>
IDrawListTextureWrap CreateDrawListTexture(string? debugName = null);
/// <summary>Creates a texture from the given existing texture, cropping and converting pixel format as needed. /// <summary>Creates a texture from the given existing texture, cropping and converting pixel format as needed.
/// </summary> /// </summary>
/// <param name="wrap">The source texture wrap. The passed value may be disposed once this function returns, /// <param name="wrap">The source texture wrap. The passed value may be disposed once this function returns,
@ -169,6 +179,14 @@ public interface ITextureProvider
string? debugName = null, string? debugName = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary>Creates a texture from clipboard.</summary>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the status of the operation.</returns>
Task<IDalamudTextureWrap> CreateFromClipboardAsync(
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets the supported bitmap decoders.</summary> /// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns> /// <returns>The supported bitmap decoders.</returns>
/// <remarks> /// <remarks>
@ -192,6 +210,11 @@ public interface ITextureProvider
/// </remarks> /// </remarks>
ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup);
/// <summary>Gets a value indicating whether the current desktop clipboard contains an image that can be attempted
/// to read using <see cref="CreateFromClipboardAsync"/>.</summary>
/// <returns><c>true</c> if it is the case.</returns>
bool HasClipboardImage();
/// <summary>Gets a shared texture corresponding to the given game resource icon specifier.</summary> /// <summary>Gets a shared texture corresponding to the given game resource icon specifier.</summary>
/// <remarks> /// <remarks>
/// <para>This function does not throw exceptions.</para> /// <para>This function does not throw exceptions.</para>

View file

@ -106,4 +106,17 @@ public interface ITextureReadbackProvider
IReadOnlyDictionary<string, object>? props = null, IReadOnlyDictionary<string, object>? props = null,
bool leaveWrapOpen = false, bool leaveWrapOpen = false,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary>Copies the texture to clipboard.</summary>
/// <param name="wrap">Texture wrap to copy.</param>
/// <param name="preferredFileNameWithoutExtension">Preferred file name.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the status of the operation.</returns>
Task CopyToClipboardAsync(
IDalamudTextureWrap wrap,
string? preferredFileNameWithoutExtension = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
} }

View file

@ -0,0 +1,40 @@
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Utility;
/// <summary>Clipboard formats, looked up by their names.</summary>
internal static class ClipboardFormats
{
/// <inheritdoc cref="CFSTR.CFSTR_FILECONTENTS"/>
public static uint FileContents { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILECONTENTS);
/// <summary>Gets the clipboard format corresponding to the PNG file format.</summary>
public static uint Png { get; } = ClipboardFormatFromName("PNG");
/// <inheritdoc cref="CFSTR.CFSTR_FILEDESCRIPTORW"/>
public static uint FileDescriptorW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORW);
/// <inheritdoc cref="CFSTR.CFSTR_FILEDESCRIPTORA"/>
public static uint FileDescriptorA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORA);
/// <inheritdoc cref="CFSTR.CFSTR_FILENAMEW"/>
public static uint FileNameW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEW);
/// <inheritdoc cref="CFSTR.CFSTR_FILENAMEA"/>
public static uint FileNameA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEA);
private static unsafe uint ClipboardFormatFromName(ReadOnlySpan<char> 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.");
}
}

View file

@ -22,8 +22,14 @@ internal class ThreadBoundTaskScheduler : TaskScheduler
public ThreadBoundTaskScheduler(Thread? boundThread = null) public ThreadBoundTaskScheduler(Thread? boundThread = null)
{ {
this.BoundThread = boundThread; this.BoundThread = boundThread;
this.TaskQueued += static () => { };
} }
/// <summary>
/// Event fired when a task has been posted.
/// </summary>
public event Action TaskQueued;
/// <summary> /// <summary>
/// Gets or sets the thread this task scheduler is bound to. /// Gets or sets the thread this task scheduler is bound to.
/// </summary> /// </summary>
@ -57,6 +63,7 @@ internal class ThreadBoundTaskScheduler : TaskScheduler
/// <inheritdoc/> /// <inheritdoc/>
protected override void QueueTask(Task task) protected override void QueueTask(Task task)
{ {
this.TaskQueued.Invoke();
this.scheduledTasks[task] = Scheduled; this.scheduledTasks[task] = Scheduled;
} }