diff --git a/ImGuiScene/D3DTextureWrap.cs b/ImGuiScene/D3D11TextureWrap.cs similarity index 62% rename from ImGuiScene/D3DTextureWrap.cs rename to ImGuiScene/D3D11TextureWrap.cs index 48656bd..217fec2 100644 --- a/ImGuiScene/D3DTextureWrap.cs +++ b/ImGuiScene/D3D11TextureWrap.cs @@ -2,36 +2,31 @@ using SharpDX.Direct3D11; using System; - /// /// DX11 Implementation of . -/// Provides a simple wrapped view of the disposeable resource as well as the handle for ImGui. +/// Provides a simple wrapped view of the disposable resource as well as the handle for ImGui. /// -public class D3DTextureWrap : TextureWrap -{ +public class D3D11TextureWrap : TextureWrap { // hold onto this directly for easier dispose etc and in case we need it later - private ShaderResourceView _resourceView = null; + private ShaderResourceView _resourceView; public int Width { get; } public int Height { get; } - public IntPtr ImGuiHandle => (_resourceView == null) ? IntPtr.Zero : _resourceView.NativePointer; + public IntPtr ImGuiHandle => _resourceView?.NativePointer ?? IntPtr.Zero; - public D3DTextureWrap(ShaderResourceView texView, int width, int height) - { + public D3D11TextureWrap(ShaderResourceView texView, int width, int height) { _resourceView = texView; Width = width; Height = height; } #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { + + private bool disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { // TODO: dispose managed state (managed objects). } @@ -45,17 +40,16 @@ protected virtual void Dispose(bool disposing) } } - ~D3DTextureWrap() - { + ~D3D11TextureWrap() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(false); } - public void Dispose() - { + public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); GC.SuppressFinalize(this); } + #endregion } diff --git a/ImGuiScene/D3D12TextureManager.cs b/ImGuiScene/D3D12TextureManager.cs new file mode 100644 index 0000000..5ad8995 --- /dev/null +++ b/ImGuiScene/D3D12TextureManager.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SharpDX.Direct3D12; +using SharpDX.DXGI; + +using Device = SharpDX.Direct3D12.Device; +using Resource = SharpDX.Direct3D12.Resource; + +using static ImGuiScene.NativeMethods; + +namespace ImGuiScene { + /// + /// Class that manages the creation, lookup, and synchronization of textures as . + /// + public class D3D12TextureManager : IDisposable { + private readonly Device _device; + private volatile bool _disposed; + + private TextureInfo[] _staticBoundTextures; + private readonly Dictionary _dynamicBoundTextures = new(); + private readonly object _textureLock = new(); + private readonly int _staticTextureCount; + private readonly int _descriptorCount; + + public D3D12TextureManager(Device device, int staticTextureCount, int maxDynamicTexturesPerFrame = 1023) { + this._device = device; + this._staticBoundTextures = new TextureInfo[staticTextureCount]; + this._descriptorCount = staticTextureCount + maxDynamicTexturesPerFrame; + this._staticTextureCount = staticTextureCount; + + this.CbvSrvHeap = this._device.CreateDescriptorHeap(new DescriptorHeapDescription { + Type = DescriptorHeapType.ConstantBufferViewShaderResourceViewUnorderedAccessView, + DescriptorCount = this._descriptorCount, + Flags = DescriptorHeapFlags.ShaderVisible + }); + } + + public DescriptorHeap CbvSrvHeap { get; } + + // TODO: Use a LRU cache to swap out textures instead of clearing the dynamic texture binds each frame. + public void ClearDynamicTextures() { + lock (this._textureLock) { + this._dynamicBoundTextures.Clear(); + } + } + + public void BindStaticTexture(D3D12TextureWrap wrap, int index) { + Debug.Assert(index < this._staticTextureCount, "index < this._staticTextureCount"); + + lock (this._textureLock) { + var (cpuHandle, _) = this.GetSrvHandles(index); + this._device.CreateShaderResourceView(wrap.Info.Resource, wrap.Info.SrvDesc, cpuHandle); + + this._staticBoundTextures[index]?.Dispose(); + this._staticBoundTextures[index] = wrap.Info; + } + } + + public unsafe D3D12TextureWrap CreateTexture(void* pixelData, int width, int height, int bytesPerPixel, + Format format) { + // TODO: Figure out if we need to implement other sizes. + Debug.Assert(bytesPerPixel == 4, "bytesPerPixel == 4"); + + // Upload texture to graphics system + var props = new HeapProperties { + Type = HeapType.Default, + CPUPageProperty = CpuPageProperty.Unknown, + MemoryPoolPreference = MemoryPool.Unknown + }; + + var desc = new ResourceDescription { + Dimension = ResourceDimension.Texture2D, + Alignment = 0, + Width = width, + Height = height, + DepthOrArraySize = 1, + MipLevels = 1, + Format = format, + SampleDescription = { Count = 1, Quality = 0 }, + Layout = TextureLayout.Unknown, + Flags = ResourceFlags.None + }; + + var texture = + this._device.CreateCommittedResource(props, HeapFlags.None, desc, ResourceStates.CopyDestination); + + var uploadPitch = (width * bytesPerPixel + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u) & + ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + var uploadSize = height * uploadPitch; + desc.Dimension = ResourceDimension.Buffer; + desc.Alignment = 0; + desc.Width = uploadSize; + desc.Height = 1; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = Format.Unknown; + desc.SampleDescription.Count = 1; + desc.SampleDescription.Quality = 0; + desc.Layout = TextureLayout.RowMajor; + desc.Flags = ResourceFlags.None; + + props.Type = HeapType.Upload; + props.CPUPageProperty = CpuPageProperty.Unknown; + props.MemoryPoolPreference = MemoryPool.Unknown; + + var uploadBuffer = + this._device.CreateCommittedResource(props, HeapFlags.None, desc, ResourceStates.GenericRead); + + var range = new Range { Begin = 0, End = uploadSize }; + var mapped = uploadBuffer.Map(0, range); + + var lenToCopy = width * bytesPerPixel; + for (var y = 0; y < height; y++) { + Buffer.MemoryCopy((void*)((IntPtr)pixelData + y * width * bytesPerPixel), + (void*)(mapped + y * (nint)uploadPitch), lenToCopy, lenToCopy); + } + + uploadBuffer.Unmap(0, range); + + var srcLocation = new TextureCopyLocation(uploadBuffer, new PlacedSubResourceFootprint { + Footprint = new SubResourceFootprint { + Format = format, + Width = width, + Height = height, + Depth = 1, + RowPitch = (int)uploadPitch, + } + }); + + var dstLocation = new TextureCopyLocation(texture, 0); + + var barrier = new ResourceBarrier { + Type = ResourceBarrierType.Transition, + Flags = ResourceBarrierFlags.None, + Transition = new ResourceTransitionBarrier(texture, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + ResourceStates.CopyDestination, ResourceStates.PixelShaderResource) + }; + + var fence = this._device.CreateFence(0, FenceFlags.None); + + var ev = CreateEvent(IntPtr.Zero, false, false, null); + Debug.Assert(ev != IntPtr.Zero, "ev != IntPtr.Zero"); + + var queueDesc = new CommandQueueDescription { + Type = CommandListType.Direct, + Flags = CommandQueueFlags.None, + NodeMask = 1 + }; + + var cmdQueue = this._device.CreateCommandQueue(queueDesc); + var cmdAlloc = this._device.CreateCommandAllocator(CommandListType.Direct); + var cmdList = this._device.CreateCommandList(0, CommandListType.Direct, cmdAlloc, null); + + cmdList.CopyTextureRegion(dstLocation, 0, 0, 0, srcLocation, null); + cmdList.ResourceBarrier(barrier); + cmdList.Close(); + + cmdQueue.ExecuteCommandLists(cmdList); + cmdQueue.Signal(fence, 1); + + fence.SetEventOnCompletion(1, ev); + WaitForSingleObject(ev, INFINITE); + + cmdList.Dispose(); + cmdAlloc.Dispose(); + cmdQueue.Dispose(); + CloseHandle(ev); + fence.Dispose(); + uploadBuffer.Dispose(); + + // Create texture view + var srvDesc = new ShaderResourceViewDescription { + Format = format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = desc.MipLevels, MostDetailedMip = 0 }, + Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING + }; + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + return new D3D12TextureWrap(new TextureInfo { + Resource = texture, + SrvDesc = srvDesc, + }, width, height, this.AwaitTextureDispose, this.GetOrBindTextureData); + } + + public void Dispose() { + this._disposed = true; + + foreach (var texture in this._staticBoundTextures) { + texture.Dispose(); + } + + this._staticBoundTextures = null; + } + + private (CpuDescriptorHandle, GpuDescriptorHandle) GetSrvHandles(int textureIndex) { + var srvHandleSize = this._device.GetDescriptorHandleIncrementSize(DescriptorHeapType + .ConstantBufferViewShaderResourceViewUnorderedAccessView); + + var cpuHandle = this.CbvSrvHeap.CPUDescriptorHandleForHeapStart; + cpuHandle.Ptr += textureIndex * srvHandleSize; + + var gpuHandle = this.CbvSrvHeap.GPUDescriptorHandleForHeapStart; + gpuHandle.Ptr += textureIndex * srvHandleSize; + + return (cpuHandle, gpuHandle); + } + + private void AwaitTextureDispose(D3D12TextureWrap wrap) { + lock (this._textureLock) { + wrap.Info.Resource.Dispose(); + wrap.Info = null; + } + } + + private IntPtr GetOrBindTextureData(D3D12TextureWrap wrap) { + if (this._disposed) return IntPtr.Zero; + + lock (this._textureLock) { + if (this._dynamicBoundTextures.TryGetValue(wrap.Info, out var handle)) { + return (IntPtr)handle.Ptr; + } + + for (var i = 0; i < this._staticBoundTextures.Length; i++) { + var info = this._staticBoundTextures[i]; + if (info == wrap.Info) { + return (IntPtr)this.GetSrvHandles(i).Item2.Ptr; + } + } + + var nextDescriptorIndex = this._staticBoundTextures.Length + this._dynamicBoundTextures.Count; + if (nextDescriptorIndex >= this._descriptorCount) { + throw new OutOfMemoryException("Ran out of heap descriptors"); + } + + var (cpuHandle, gpuHandle) = this.GetSrvHandles(nextDescriptorIndex); + this._device.CreateShaderResourceView(wrap.Info.Resource, wrap.Info.SrvDesc, cpuHandle); + this._dynamicBoundTextures.Add(wrap.Info, gpuHandle); + return (IntPtr)gpuHandle.Ptr; + } + } + + internal record TextureInfo : IDisposable { + public Resource Resource; + public ShaderResourceViewDescription SrvDesc; + + public void Dispose() { + this.Resource.Dispose(); + } + } + } +} diff --git a/ImGuiScene/D3D12TextureWrap.cs b/ImGuiScene/D3D12TextureWrap.cs new file mode 100644 index 0000000..db58f2f --- /dev/null +++ b/ImGuiScene/D3D12TextureWrap.cs @@ -0,0 +1,58 @@ +using System; + +namespace ImGuiScene { + /// + /// DX12 Implementation of . + /// Provides a simple wrapped view of the disposable resource as well as the handle for ImGui. + /// + public class D3D12TextureWrap : TextureWrap { + private readonly Action _disposing; + private readonly Func _getImGuiHandle; + + internal D3D12TextureManager.TextureInfo Info { get; set; } + + public int Width { get; } + public int Height { get; } + public IntPtr ImGuiHandle => this._getImGuiHandle(this); + + internal D3D12TextureWrap(D3D12TextureManager.TextureInfo info, int width, int height, + Action disposing, + Func getImGuiHandle) { + this.Info = info; + this.Width = width; + this.Height = height; + this._disposing = disposing; + this._getImGuiHandle = getImGuiHandle; + } + + #region IDisposable Support + + private bool _disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) { + if (this._disposedValue) return; + + if (disposing) { + this._disposing(this); + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + this._disposedValue = true; + } + + ~D3D12TextureWrap() { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(false); + } + + public void Dispose() { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/ImGuiScene/ImGuiScene.csproj b/ImGuiScene/ImGuiScene.csproj index 4d6de87..9a317f3 100644 --- a/ImGuiScene/ImGuiScene.csproj +++ b/ImGuiScene/ImGuiScene.csproj @@ -51,6 +51,7 @@ + diff --git a/ImGuiScene/ImGui_Impl/Custom.cs b/ImGuiScene/ImGui_Impl/Custom.cs new file mode 100644 index 0000000..87cda8c --- /dev/null +++ b/ImGuiScene/ImGui_Impl/Custom.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace ImGuiScene.ImGui_Impl { + // Custom cimgui functions we use for utility purposes + internal static class Custom { + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern void igCustom_ClearStacks(); + } +} diff --git a/ImGuiScene/ImGui_Impl/Native/ImGui_ImplDX12_Native.cs b/ImGuiScene/ImGui_Impl/Native/ImGui_ImplDX12_Native.cs new file mode 100644 index 0000000..4e4d1df --- /dev/null +++ b/ImGuiScene/ImGui_Impl/Native/ImGui_ImplDX12_Native.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using ImGuiNET; +using SharpDX.Direct3D12; +using SharpDX.DXGI; + +using Device = SharpDX.Direct3D12.Device; +using Resource = SharpDX.Direct3D12.Resource; + +using static ImGuiScene.NativeMethods; + +namespace ImGuiScene.ImGui_Impl.Native { + public unsafe class ImGui_ImplDX12_Native : IDisposable { + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern bool igImpls_ImplDX12_Init(IntPtr device, int numFramesInFlight, Format rtvFormat, + IntPtr cbvSrvHeap, CpuDescriptorHandle fontSrvCpuDescHandle, GpuDescriptorHandle fontSrvGpuDescHandle); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern void igImpls_ImplDX12_Shutdown(); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern void igImpls_ImplDX12_NewFrame(); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern void igImpls_ImplDX12_RenderDrawData(ImDrawData* drawData, IntPtr graphicsCommandList); + + // Only needed to support recreating the device, doubt we need this ever + /* + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern void igImpls_ImplDX12_InvalidateDeviceObjects(); + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern bool igImpls_ImplDX12_CreateDeviceObjects(); + */ + + private Device _device; + private SwapChain3 _swapChain; + + // TODO: Detect format from render target? + private const Format DXGI_FORMAT = Format.R8G8B8A8_UNorm; + + private D3D12TextureManager _textureManager; + private List _fontTextures = new(); + private DescriptorHeap _rtvDescriptorHeap; + private readonly List<(Resource, CpuDescriptorHandle)> _mainRtv = new(); + + private CommandQueue _commandQueue; + private CommandAllocator _commandAllocator; + private GraphicsCommandList _commandList; + + private Fence _fence; + private IntPtr _fenceEvent; + private long _fenceValue; + + public void Init(Device device, SwapChain3 swapChain, CommandQueue commandQueue, int numFramesInFlight) { + this._device = device; + this._swapChain = swapChain; + // Reserve one static descriptor for the ImGui font. + // TODO: Make this work for multiple ImGui fonts? + this._textureManager = new D3D12TextureManager(device, 1); + + var init = igImpls_ImplDX12_Init( + device.NativePointer, + numFramesInFlight, + DXGI_FORMAT, + this._textureManager.CbvSrvHeap.NativePointer, + this._textureManager.CbvSrvHeap.CPUDescriptorHandleForHeapStart, + this._textureManager.CbvSrvHeap.GPUDescriptorHandleForHeapStart + ); + Debug.Assert(init, "Couldn't init native dx12 backend"); + + this._commandQueue = commandQueue; + this._commandAllocator = this._device.CreateCommandAllocator(CommandListType.Direct); + this._commandList = this._device.CreateCommandList(CommandListType.Direct, this._commandAllocator, null); + this._commandList.Close(); + + this._fence = this._device.CreateFence(this._fenceValue, FenceFlags.None); + this._fenceEvent = CreateEvent(IntPtr.Zero, false, false, null); + } + + public void NewFrame() { + this._textureManager.ClearDynamicTextures(); + igImpls_ImplDX12_NewFrame(); + } + + public void RenderDrawData(ImDrawDataPtr drawDataPtr) { + if (this._mainRtv.Count == 0) + return; + + var backBufferIndex = this._swapChain.CurrentBackBufferIndex; + this._commandAllocator.Reset(); + + var barrier = new ResourceBarrier { + Type = ResourceBarrierType.Transition, + Flags = ResourceBarrierFlags.None, + Transition = new ResourceTransitionBarrier(this._mainRtv[backBufferIndex].Item1, + D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, ResourceStates.Present, ResourceStates.RenderTarget), + }; + this._commandList.Reset(this._commandAllocator, null); + this._commandList.ResourceBarrier(barrier); + + this._commandList.SetRenderTargets(this._mainRtv[backBufferIndex].Item2, null); + // TODO: Idk if this is necessary since ImGui might do it? Test without it. + this._commandList.SetDescriptorHeaps(this._textureManager.CbvSrvHeap); + + igImpls_ImplDX12_RenderDrawData(drawDataPtr.NativePtr, this._commandList.NativePointer); + + barrier.Transition = new ResourceTransitionBarrier(this._mainRtv[backBufferIndex].Item1, + D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, ResourceStates.RenderTarget, ResourceStates.Present); + this._commandList.ResourceBarrier(barrier); + this._commandList.Close(); + + this._commandQueue.ExecuteCommandLists(this._commandList); + } + + public void Shutdown() { + this.WaitForLastSubmittedFrame(); + igImpls_ImplDX12_Shutdown(); + } + + public void OnPostPresent() { + var fenceValue = this._fenceValue + 1; + this._commandQueue.Signal(this._fence, fenceValue); + this._fenceValue = fenceValue; + } + + public void InvalidateRenderTargets() { + foreach (var (resource, _) in this._mainRtv) { + resource.Dispose(); + } + + this._mainRtv.Clear(); + this._rtvDescriptorHeap?.Dispose(); + this._rtvDescriptorHeap = null; + } + + public void CreateRenderTargets(int bufferCount) { + this.InvalidateRenderTargets(); + + var desc = new DescriptorHeapDescription { + Type = DescriptorHeapType.RenderTargetView, + DescriptorCount = bufferCount, + Flags = DescriptorHeapFlags.None, + NodeMask = 1 + }; + + this._rtvDescriptorHeap = this._device.CreateDescriptorHeap(desc); + + var rtvDescriptorSize = + this._device.GetDescriptorHandleIncrementSize(DescriptorHeapType.RenderTargetView); + var rtvHandle = this._rtvDescriptorHeap.CPUDescriptorHandleForHeapStart; + + for (var i = 0; i < bufferCount; i++) { + var backBuffer = this._swapChain.GetBackBuffer(i); + + Debug.Assert(backBuffer is not null, "backBuffer was null in CreateRenderTargets"); + + this._device.CreateRenderTargetView(backBuffer, null, rtvHandle); + this._mainRtv.Add((backBuffer, rtvHandle)); + + rtvHandle.Ptr += rtvDescriptorSize; + } + } + + public TextureWrap CreateTexture(void* pixelData, int width, int height, int bytesPerPixel) { + return this._textureManager.CreateTexture(pixelData, width, height, bytesPerPixel, DXGI_FORMAT); + } + + // Added to support dynamic rebuilding of the font texture + // for adding fonts after initialization time + public void RebuildFontTexture() { + foreach (var font in this._fontTextures) { + font.Dispose(); + } + + this._fontTextures.Clear(); + this.CreateFontsTexture(); + } + + private void WaitForLastSubmittedFrame() { + var fenceValue = this._fenceValue; + if (fenceValue == 0) + return; // No fence was signaled + + this._fenceValue = 0; + if (this._fence.CompletedValue >= fenceValue) + return; + + this._fence.SetEventOnCompletion(fenceValue, this._fenceEvent); + // TODO: I'm not sure if we should wait infinitely here as sometimes the game can crash and not render any more frames meaning this deadlocks. + WaitForSingleObject(this._fenceEvent, INFINITE); + } + + private void CreateFontsTexture() { + var io = ImGui.GetIO(); + if (io.Fonts.Textures.Size == 0) + io.Fonts.Build(); + + for (int textureIndex = 0, textureCount = io.Fonts.Textures.Size; + textureIndex < textureCount; + textureIndex++) { + // Build texture atlas + io.Fonts.GetTexDataAsRGBA32(textureIndex, out IntPtr fontPixels, out var fontWidth, + out var fontHeight, + out var fontBytesPerPixel); + + var wrap = this._textureManager.CreateTexture((void*)fontPixels, fontWidth, fontHeight, + fontBytesPerPixel, DXGI_FORMAT); + // TODO: Multiple fonts? + this._textureManager.BindStaticTexture(wrap, textureIndex); + + this._fontTextures.Add(wrap); + // TODO: I'm pretty sure this just overrides the global font over and over. Verify. + io.Fonts.SetTexID(textureIndex, wrap.ImGuiHandle); + } + + io.Fonts.ClearTexData(); + } + + public void Dispose() { + CloseHandle(this._fenceEvent); + this._fence.Dispose(); + this.InvalidateRenderTargets(); + this._fontTextures.Clear(); + this._textureManager.Dispose(); + } + } +} diff --git a/ImGuiScene/NativeMethods.cs b/ImGuiScene/NativeMethods.cs new file mode 100644 index 0000000..501308d --- /dev/null +++ b/ImGuiScene/NativeMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security; + +namespace ImGuiScene +{ + internal static class NativeMethods + { + [DllImport("kernel32.dll")] + public static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); + + [DllImport("kernel32.dll", SetLastError=true)] + public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll")] + public static extern uint WaitForMultipleObjects(uint nCount, IntPtr[] lpHandles, bool bWaitAll, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError=true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [SuppressUnmanagedCodeSecurity] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + public const int D3D12_TEXTURE_DATA_PITCH_ALIGNMENT = 256; + public const int D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES = -1; + public const int D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING = 5768; + public const uint INFINITE = 0xFFFFFFFF; + } +} \ No newline at end of file diff --git a/ImGuiScene/RawDX11Scene.cs b/ImGuiScene/RawDX11Scene.cs index d01012d..7f6b77c 100644 --- a/ImGuiScene/RawDX11Scene.cs +++ b/ImGuiScene/RawDX11Scene.cs @@ -8,8 +8,10 @@ using System; using System.IO; using ImGuiScene.ImGui_Impl.Native; +using ImGuiScene.ImGui_Impl; using ImGuizmoNET; using ImPlotNET; + using Device = SharpDX.Direct3D11.Device; namespace ImGuiScene @@ -178,6 +180,12 @@ public void InvalidateFonts() this.imguiRenderer.RebuildFontTexture(); } + // It is pretty much required that this is called from a handler attached + // to OnNewRenderFrame + public void ClearStacksOnContext() { + Custom.igCustom_ClearStacks(); + } + public bool IsImGuiCursor(IntPtr hCursor) { return this.imguiInput.IsImGuiCursor(hCursor); @@ -256,7 +264,7 @@ private unsafe TextureWrap CreateTexture(void* pixelData, int width, int height, // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new D3DTextureWrap(resView, width, height); + return new D3D11TextureWrap(resView, width, height); } public byte[] CaptureScreenshot() @@ -357,4 +365,4 @@ public void Dispose() } #endregion } -} \ No newline at end of file +} diff --git a/ImGuiScene/RawDX12Scene.cs b/ImGuiScene/RawDX12Scene.cs new file mode 100644 index 0000000..453242b --- /dev/null +++ b/ImGuiScene/RawDX12Scene.cs @@ -0,0 +1,302 @@ +using ImGuiNET; +using PInvoke; +using SharpDX.Direct3D12; +using SharpDX.DXGI; +using StbiSharp; +using System; +using System.ComponentModel; +using System.IO; +using ImGuiScene.ImGui_Impl; +using ImGuiScene.ImGui_Impl.Native; +using ImGuizmoNET; +using ImPlotNET; + +using Device = SharpDX.Direct3D12.Device; + +namespace ImGuiScene { + // This class will likely eventually be unified a bit more with other scenes, but for + // now it should be directly useable + public sealed class RawDX12Scene : IDisposable { + public Device Device { get; private set; } + public IntPtr WindowHandlePtr { get; private set; } + + // IDXGISwapChain3 is required to call GetCurrentBackBufferIndex. + public SwapChain3 SwapChain { get; private set; } + public CommandQueue CommandQueue { get; private set; } + + public bool UpdateCursor { + get => this.imguiInput.UpdateCursor; + set => this.imguiInput.UpdateCursor = value; + } + + private int targetWidth; + private int targetHeight; + + private ImGui_ImplDX12_Native imguiRenderer; + private ImGui_Input_Impl_Direct imguiInput; + + public delegate void BuildUIDelegate(); + + public delegate void NewInputFrameDelegate(); + + public delegate void NewRenderFrameDelegate(); + + /// + /// User methods invoked every ImGui frame to construct custom UIs. + /// + public BuildUIDelegate OnBuildUI; + + public NewInputFrameDelegate OnNewInputFrame; + public NewRenderFrameDelegate OnNewRenderFrame; + + private string imguiIniPath; + + public string ImGuiIniPath { + get => this.imguiIniPath; + set { + this.imguiIniPath = value; + this.imguiInput.SetIniPath(this.imguiIniPath); + } + } + + /// + /// Creates an instance of the class . + /// + /// Pointer to an IDXGISwapChain. + /// Pointer to the ID3D12CommandQueue used to initialize + /// Pointer to a native ID3D12Device. By default the device will be derived from . + /// + /// Ensure was the command queue used to initialize , otherwise rendering will crash. + /// + public RawDX12Scene(IntPtr nativeSwapChain, IntPtr nativeCommandQueue, IntPtr? nativeDevice = null) { + this.SwapChain = new SwapChain(nativeSwapChain).QueryInterfaceOrNull() + ?? throw new InvalidEnumArgumentException("Failed to query SwapChain3 interface"); + this.CommandQueue = new CommandQueue(nativeCommandQueue); + this.Device = nativeDevice is null ? this.SwapChain.GetDevice() : new Device((IntPtr)nativeDevice); + + this.Initialize(); + } + + private void Initialize() { + // could also do things with GetClientRect() for WindowHandlePtr, not sure if that is necessary + this.targetWidth = this.SwapChain.Description.ModeDescription.Width; + this.targetHeight = this.SwapChain.Description.ModeDescription.Height; + + this.WindowHandlePtr = this.SwapChain.Description.OutputHandle; + + this.InitializeImGui(); + } + + private void InitializeImGui() { + this.imguiRenderer = new ImGui_ImplDX12_Native(); + + var ctx = ImGui.CreateContext(); + ImGuizmo.SetImGuiContext(ctx); + ImPlot.SetImGuiContext(ctx); + ImPlot.CreateContext(); + + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable; + + this.imguiRenderer.Init(this.Device, this.SwapChain, this.CommandQueue, 1); + this.imguiInput = new ImGui_Input_Impl_Direct(this.WindowHandlePtr); + } + + /// + /// Processes window messages. + /// + /// Handle of the window. + /// Type of window message. + /// wParam. + /// lParam. + /// Return value. + public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParam, void* lParam) { + return this.imguiInput.ProcessWndProcW(hWnd, msg, wParam, lParam); + } + + public void Render() { + this.imguiRenderer.NewFrame(); + this.OnNewRenderFrame?.Invoke(); + this.imguiInput.NewFrame(targetWidth, targetHeight); + this.OnNewInputFrame?.Invoke(); + + ImGui.NewFrame(); + ImGuizmo.BeginFrame(); + + this.OnBuildUI?.Invoke(); + + ImGui.Render(); + + this.imguiRenderer.RenderDrawData(ImGui.GetDrawData()); + ImGui.UpdatePlatformWindows(); + ImGui.RenderPlatformWindowsDefault(); + } + + /// + /// This should be called after the swapchain present call has finished. + /// + public void OnPostPresent() { + this.imguiRenderer.OnPostPresent(); + } + + public void OnPreResize() { + this.imguiRenderer.InvalidateRenderTargets(); + } + + // TODO: If resize buffers is never called we don't initialize the render view targets, maybe add OnPostGetBuffer? + public void OnPostResize(int bufferCount, int newWidth, int newHeight, int newFormat) { + this.imguiRenderer.CreateRenderTargets(bufferCount); + + this.targetWidth = newWidth; + this.targetHeight = newHeight; + } + + // It is pretty much required that this is called from a handler attached + // to OnNewRenderFrame + public void InvalidateFonts() { + // TODO + this.imguiRenderer.RebuildFontTexture(); + } + + // It is pretty much required that this is called from a handler attached + // to OnNewRenderFrame + public void ClearStacksOnContext() { + Custom.igCustom_ClearStacks(); + } + + public bool IsImGuiCursor(IntPtr hCursor) { + return this.imguiInput.IsImGuiCursor(hCursor); + } + + public TextureWrap LoadImage(string path) { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); + using var ms = new MemoryStream(); + fs.CopyTo(ms); + var image = Stbi.LoadFromMemory(ms, 4); + return this.LoadImage_Internal(image); + } + + public TextureWrap LoadImage(byte[] imageBytes) { + using var ms = new MemoryStream(imageBytes, 0, imageBytes.Length, false, true); + var image = Stbi.LoadFromMemory(ms, 4); + return this.LoadImage_Internal(image); + } + + public unsafe TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels = 4) { + // StbiSharp doesn't expose a constructor, even just to wrap existing data, which means + // short of something awful like below, or creating another wrapper layer, we can't avoid + // adding divergent code paths into CreateTexture + //var mock = new { Width = width, Height = height, NumChannels = numChannels, Data = imageData }; + //var image = Unsafe.As(mock); + //return LoadImage_Internal(image); + + fixed (void* pixelData = imageData) { + return this.CreateTexture(pixelData, width, height, numChannels); + } + } + + private unsafe TextureWrap LoadImage_Internal(StbiImage image) { + fixed (void* pixelData = image.Data) { + return this.CreateTexture(pixelData, image.Width, image.Height, image.NumChannels); + } + } + + private unsafe TextureWrap CreateTexture(void* pixelData, int width, int height, int bytesPerPixel) { + return this.imguiRenderer.CreateTexture(pixelData, width, height, bytesPerPixel); + } + + public byte[] CaptureScreenshot() { + throw new NotImplementedException(); + // using (var backBuffer = this.SwapChain.GetBackBuffer(0)) + // { + // Texture2DDescription desc = backBuffer.Description; + // desc.CpuAccessFlags = CpuAccessFlags.Read; + // desc.Usage = ResourceUsage.Staging; + // desc.OptionFlags = ResourceOptionFlags.None; + // desc.BindFlags = BindFlags.None; + // + // using (var tex = new Texture2D(this.Device, desc)) + // { + // this.deviceContext.CopyResource(backBuffer, tex); + // using (var surf = tex.QueryInterface()) + // { + // var map = surf.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream); + // var pixelData = new byte[surf.Description.Width * surf.Description.Height * surf.Description.Format.SizeOfInBytes()]; + // var dataCounter = 0; + // + // while (dataCounter < pixelData.Length) + // { + // //var curPixel = dataStream.Read(); + // var x = dataStream.Read(); + // var y = dataStream.Read(); + // var z = dataStream.Read(); + // var w = dataStream.Read(); + // + // pixelData[dataCounter++] = z; + // pixelData[dataCounter++] = y; + // pixelData[dataCounter++] = x; + // pixelData[dataCounter++] = w; + // } + // + // // TODO: test this on a thread + // //var gch = GCHandle.Alloc(pixelData, GCHandleType.Pinned); + // //using (var bitmap = new Bitmap(surf.Description.Width, surf.Description.Height, map.Pitch, PixelFormat.Format32bppRgb, gch.AddrOfPinnedObject())) + // //{ + // // bitmap.Save(path); + // //} + // //gch.Free(); + // + // surf.Unmap(); + // dataStream.Dispose(); + // + // return pixelData; + // } + // } + // } + } + + #region IDisposable Support + + private bool disposedValue; // To detect redundant calls + + private void Dispose(bool disposing) { + if (this.disposedValue) return; + + if (disposing) { + // TODO: dispose managed state (managed objects). + } + + this.imguiRenderer?.Shutdown(); + this.imguiInput?.Dispose(); + + ImGui.DestroyContext(); + + this.imguiRenderer?.Dispose(); + + // Not actually sure how sharpdx does ref management, but hopefully they + // addref when we create our wrappers, so this should just release that count + + // Originally it was thought these lines were needed because it was assumed that SharpDX does + // proper refcounting to handle disposing, but disposing these would cause the game to crash + // on resizing after unloading Dalamud + // this.SwapChain?.Dispose(); + // this.deviceContext?.Dispose(); + // this.Device?.Dispose(); + + this.disposedValue = true; + } + + ~RawDX12Scene() { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/ImGuiScene/Renderers/SimpleD3D.cs b/ImGuiScene/Renderers/SimpleD3D.cs index 562afaf..95fb309 100644 --- a/ImGuiScene/Renderers/SimpleD3D.cs +++ b/ImGuiScene/Renderers/SimpleD3D.cs @@ -170,7 +170,7 @@ public unsafe TextureWrap CreateTexture(void* pixelData, int width, int height, // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new D3DTextureWrap(resView, width, height); + return new D3D11TextureWrap(resView, width, height); } #region ImGui forwarding