using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using ImGuiNET; using SharpDX.D3DCompiler; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Mathematics.Interop; using T3.Core.Operator.Slots; using T3.Core.Rendering; using T3.Core.Resource; using T3.Core.SystemUi; using T3.Editor.App; using T3.Editor.Gui; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows; using T3.Editor.Gui.Windows.Analyze; using T3.Editor.SystemUi; using T3.SystemUi; using Buffer = SharpDX.Direct3D11.Buffer; using Device = SharpDX.Direct3D11.Device; using Vector2 = System.Numerics.Vector2; namespace T3.Editor.UiContentDrawing; /// /// Renders draw data generated by ImGui with SharpDX render form /// internal sealed class WindowsUiContentDrawer : IUiContentDrawer { private IntPtr _imguiContext; private object _contextLock; private readonly ShaderResourceView _customImageView = new ShaderResourceView(IntPtr.Zero); #region Init public void Initialize(Device device, int width, int height, object contextLock, out IntPtr imguiContext) { if (device == null) { Log.Error("Can't initialize window without device."); BlockingWindow.Instance.ShowMessageBox("Can't initialize rendering device.", "Graphics error"); EditorUi.Instance.ExitApplication(); imguiContext = IntPtr.Zero; return; } _device = device; _deviceContext = device.ImmediateContext; _windowWidth = width; _windowHeight = height; _contextLock = contextLock; lock (_contextLock) { IntPtr previousContext; try { previousContext = ImGui.GetCurrentContext(); } catch { // ignored previousContext = IntPtr.Zero; } _imguiContext = ImGui.CreateContext(); ImGui.SetCurrentContext(_imguiContext); SetPerFrameImGuiData(1f / 60f); var io = ImGui.GetIO(); io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.RendererHasVtxOffset; io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; // ImGui 1.91 error-recovery: stack imbalances (Push/Pop, Begin/End, etc.) // are auto-fixed and logged. Recoverable errors appear as: // - Red warning bar in the editor (ConfigErrorRecoveryEnableTooltip) // - [imgui-error] lines in Rider's terminal/stderr (ConfigErrorRecoveryEnableDebugLog) // Hard asserts (zero-size buttons, invalid flags) are caught by our // try/catch(SEHException) around the render loop. // // TODO: To get recoverable errors into the managed Log.Warning, build a tiny // native shim that hooks ImGuiContext.ErrorCallback and forwards messages to // a managed delegate via P/Invoke. See imgui.cpp:10611. io.ConfigErrorRecovery = true; io.ConfigErrorRecoveryEnableDebugLog = true; io.ConfigErrorRecoveryEnableTooltip = true; io.ConfigErrorRecoveryEnableAssert = false; // default is true; suppress native dialog // io.ConfigDebugHighlightIdConflicts = false; // enable this to suppress ID conflict overlay // restore previous context if (previousContext != IntPtr.Zero) { ImGui.SetCurrentContext(previousContext); } } imguiContext = _imguiContext; } #endregion #region Rendering public void RenderCallback() { // An attempt to prevent System.ObjectDisposedException if (Program.IsShuttingDown) return; // Frame-pacing wait via DXGI's FrameLatencyWaitableObject. Must run *before* any other // per-frame work so the loop is locked to the swap chain's signalled cadence rather than // queueing up speculatively. No-op if the waitable wasn't enabled at swap-chain creation. ProgramWindows.Main.WaitForFrameLatency(); lock (_contextLock) { ImGui.SetCurrentContext(_imguiContext); UiContentUpdate.SetupResourcesAndFontsWithScaling(); if (ProgramWindows.Main.IsMinimized && UserSettings.Config.SuspendRenderingWhenHidden) { Thread.Sleep(100); return; } UiContentUpdate.TakeMeasurement(); ImGui.GetIO().DisplaySize = ProgramWindows.Main.Size; ProgramWindows.HandleFullscreenToggle(); DirtyFlag.IncrementGlobalTicks(); T3Metrics.UiRenderingStarted(); if (!string.IsNullOrEmpty(Program.NewImGuiLayoutDefinition)) { ImGui.LoadIniSettingsFromMemory(Program.NewImGuiLayoutDefinition); Program.NewImGuiLayoutDefinition = string.Empty; } ImGui.NewFrame(); // Render 2nd view ProgramWindows.Viewer.SetVisible(T3Ui.ShowSecondaryRenderWindow); if (T3Ui.ShowSecondaryRenderWindow) { var viewer = ProgramWindows.Viewer; ProgramWindows.Viewer.PrepareRenderingFrame(); ProgramWindows.SetVertexShader(SharedResources.FullScreenVertexShaderResource); ProgramWindows.SetPixelShader(SharedResources.FullScreenPixelShaderResource); if (UserSettings.Config.MirrorUiOnSecondView) { ProgramWindows.RebuildUiCopyTextureIfRequired(); ProgramWindows.CopyUiContentToShareTexture(); if (ProgramWindows.UiCopyTextureSrv != null && !ProgramWindows.UiCopyTextureSrv.IsDisposed) { ProgramWindows.SetRasterizerState(SharedResources.ViewWindowRasterizerState); ProgramWindows.SetPixelShaderSRV(ProgramWindows.UiCopyTextureSrv); ProgramWindows.DrawTextureToSecondaryRenderOutput(); } } else { if (viewer.Texture is { IsDisposed: false }) { if (_viewWindowBackgroundSrv == null || _viewWindowBackgroundSrv.Resource.NativePointer != viewer.Texture.NativePointer) { _viewWindowBackgroundSrv?.Dispose(); _viewWindowBackgroundSrv = new ShaderResourceView(Program.Device, viewer.Texture); } ProgramWindows.SetRasterizerState(SharedResources.ViewWindowRasterizerState); ProgramWindows.SetPixelShaderSRV(_viewWindowBackgroundSrv); ProgramWindows.DrawTextureToSecondaryRenderOutput(); } else { Log.Debug($"Null {nameof(ShaderResourceView)} for 2nd render view"); } } } ProgramWindows.Main.PrepareRenderingFrame(); // Clear the main window buffer for next frame try { T3Ui.ProcessFrame(); ProgramWindows.RefreshViewport(); ImGui.Render(); ProgramWindows.Main.Form.ApplyRequestedCursor(); RenderDrawData(ImGui.GetDrawData()); } catch (SEHException e) { // A native ImGui assertion fired (e.g. SetCursorPos extent check, // invalid BeginChild flags, empty-stack pop). Log the full managed // stack trace so the call site is visible in the console/log file // alongside the [imgui-error] line that ImGui prints to stderr. Log.Error($"ImGui native assertion failed (frame skipped):\n{e}"); // Try to end the frame so the next NewFrame doesn't assert on // "NewFrame called without Render". try { ImGui.EndFrame(); } catch { /* best-effort cleanup */ } } } T3Metrics.UiRenderingCompleted(); ProgramWindows.Present(T3Ui.UseVSync, T3Ui.ShowSecondaryRenderWindow); } public void InitializeScaling() { ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DpiEnableScaleFonts; ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DpiEnableScaleViewports; unsafe { // Disable ImGui ini file settings ImGui.GetIO().NativePtr->IniFilename = null; } } public void RenderDrawData(ImDrawDataPtr drawData) { lock (_contextLock) { PrepareData(drawData); DrawData(drawData); } } private void PrepareData(ImDrawDataPtr drawData) { if (_vb == null || _vertexBufferSize < drawData.TotalVtxCount) { DisposeObj(ref _vb); _vertexBufferSize = drawData.TotalVtxCount + 5000; _vb = new Buffer(_device, new BufferDescription() { SizeInBytes = _vertexBufferSize * Unsafe.SizeOf(), Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.VertexBuffer, CpuAccessFlags = CpuAccessFlags.Write }); } if (_ib == null || _indexBufferSize < drawData.TotalIdxCount) { DisposeObj(ref _ib); _indexBufferSize = drawData.TotalIdxCount + 10000; _ib = new Buffer(_device, new BufferDescription() { SizeInBytes = _indexBufferSize * Unsafe.SizeOf(), Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.IndexBuffer, CpuAccessFlags = CpuAccessFlags.Write }); } // Copy and convert all vertices into a single contiguous buffer _deviceContext.MapSubresource(_vb, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out var vbStream); _deviceContext.MapSubresource(_ib, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out var ibStream); for (int n = 0; n < drawData.CmdListsCount; n++) { ImDrawListPtr cmdList = drawData.CmdLists[n]; vbStream.WriteRange(cmdList.VtxBuffer.Data, (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf())); ibStream.WriteRange(cmdList.IdxBuffer.Data, (uint)(cmdList.IdxBuffer.Size * Unsafe.SizeOf())); } vbStream.Dispose(); ibStream.Dispose(); _deviceContext.UnmapSubresource(_vb, 0); _deviceContext.UnmapSubresource(_ib, 0); } private readonly SharpDX.Mathematics.Interop.RawRectangle[] _prevScissorRects = new SharpDX.Mathematics.Interop.RawRectangle[16]; //Note : mrvux : unless you use multi viewport, you can set to 1, I leave 16 for safety here since it's only called once per frame, better than accumulating GC private readonly SharpDX.Mathematics.Interop.RawViewportF[] _prevViewports = new SharpDX.Mathematics.Interop.RawViewportF[16]; private void DrawData(ImDrawDataPtr drawData) { // Setup orthographic projection matrix into our constant buffer // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). var imGuiIO = ImGui.GetIO(); var projectionMatrix = Matrix4x4.CreateOrthographicOffCenter(0.0f, imGuiIO.DisplaySize.X, imGuiIO.DisplaySize.Y, 0.0f, -1.0f, 1.0f); ResourceUtils.WriteDynamicBufferData(_deviceContext, _vertexConstantBuffer, projectionMatrix); // Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!) _deviceContext.Rasterizer.GetScissorRectangles(_prevScissorRects); _deviceContext.Rasterizer.GetViewports(_prevViewports); var prevRasterizerState = _deviceContext.Rasterizer.State; var prevBlendState = _deviceContext.OutputMerger.BlendState; var prevBlendFactor = _deviceContext.OutputMerger.BlendFactor; var prevSampleMask = _deviceContext.OutputMerger.BlendSampleMask; var prevDepthStencilState = _deviceContext.OutputMerger.DepthStencilState; var prevStencilRef = _deviceContext.OutputMerger.DepthStencilReference; var prevPsShaderResource = _deviceContext.PixelShader.GetShaderResources(0, 1)[0]; var prevPsSampler = _deviceContext.PixelShader.GetSamplers(0, 1); var prevPs = _deviceContext.PixelShader.Get(); var prevVs = _deviceContext.VertexShader.Get(); var prevHs = _deviceContext.HullShader.Get(); var prevGs = _deviceContext.GeometryShader.Get(); var prevDs = _deviceContext.DomainShader.Get(); var prevVsConstantBuffer = _deviceContext.VertexShader.GetConstantBuffers(0, 1); var prevPrimitiveTopology = _deviceContext.InputAssembler.PrimitiveTopology; _deviceContext.InputAssembler.GetIndexBuffer(out var prevIndexBuffer, out var prevIndexBufferFormat, out var prevIndexBufferOffset); Buffer[] prevVertexBuffer = new Buffer[1]; int[] prevVertexBufferOffset = new int[1], prevVertexBufferStride = new int[1]; _deviceContext.InputAssembler.GetVertexBuffers(0, 1, prevVertexBuffer, prevVertexBufferOffset, prevVertexBufferStride); var prevInputLayout = _deviceContext.InputAssembler.InputLayout; // Setup viewport _deviceContext.Rasterizer.SetViewport(0, 0, drawData.DisplaySize.X, drawData.DisplaySize.Y); // Bind shader and vertex buffers int stride = Unsafe.SizeOf(); int offset = 0; _deviceContext.InputAssembler.InputLayout = _inputLayout; _deviceContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_vb, stride, offset)); _deviceContext.InputAssembler.SetIndexBuffer(_ib, Format.R16_UInt, 0); _deviceContext.InputAssembler.PrimitiveTopology = SharpDX.Direct3D.PrimitiveTopology.TriangleList; _deviceContext.VertexShader.SetShader(_vertexShader, null, 0); _deviceContext.VertexShader.SetConstantBuffer(0, _vertexConstantBuffer); _deviceContext.PixelShader.SetShader(_pixelShader, null, 0); _deviceContext.PixelShader.SetSampler(0, _fontSampler); //make sure we have no tessel/gs _deviceContext.HullShader.Set(null); _deviceContext.DomainShader.Set(null); _deviceContext.GeometryShader.Set(null); // Setup render state // Note : mrvux do not use properties for blend state / blend, since the native functions are packed with the _deviceContext.OutputMerger.SetBlendState(_blendState, new RawColor4(0.0f, 0.0f, 0.0f, 0.0f)); //sample mask to -1, no GC _deviceContext.OutputMerger.SetDepthStencilState(_depthStencilState, 0); _deviceContext.Rasterizer.State = _rasterizerState; // Render command lists int vtxOffset = 0; int idxOffset = 0; Vector2 pos = drawData.DisplayPos; for (int n = 0; n < drawData.CmdListsCount; n++) { var cmdList = drawData.CmdLists[n]; for (int cmdI = 0; cmdI < cmdList.CmdBuffer.Size; cmdI++) { var cmd = cmdList.CmdBuffer[cmdI]; if (cmd.UserCallback != IntPtr.Zero) { throw new NotImplementedException(); } else { _deviceContext.Rasterizer.SetScissorRectangle((int)(cmd.ClipRect.X - pos.X), (int)(cmd.ClipRect.Y - pos.Y), (int)(cmd.ClipRect.Z - pos.X), (int)(cmd.ClipRect.W - pos.Y)); //This set native pointer without using QueryInterface or new, which allows a "GC free" cast _customImageView.NativePointer = cmd.TextureId; _deviceContext.PixelShader.SetShaderResource(0, _customImageView); _deviceContext.DrawIndexed((int)cmd.ElemCount, idxOffset + (int)cmd.IdxOffset, vtxOffset + (int)cmd.VtxOffset); //Set to IntPtr.Zero since that would create issue when disposing (on application Exit) //Internally it only resets the pointer and deref the device if it was queried (which in this case, did not) _customImageView.NativePointer = IntPtr.Zero; } } idxOffset += cmdList.IdxBuffer.Size; vtxOffset += cmdList.VtxBuffer.Size; } // Restore modified DX state _deviceContext.Rasterizer.SetScissorRectangles(_prevScissorRects); _deviceContext.Rasterizer.SetViewports(_prevViewports); _deviceContext.Rasterizer.State = prevRasterizerState; _deviceContext.OutputMerger.BlendState = prevBlendState; _deviceContext.OutputMerger.BlendFactor = prevBlendFactor; _deviceContext.OutputMerger.BlendSampleMask = prevSampleMask; _deviceContext.OutputMerger.DepthStencilState = prevDepthStencilState; _deviceContext.OutputMerger.DepthStencilReference = prevStencilRef; _deviceContext.PixelShader.SetShaderResources(0, prevPsShaderResource); _deviceContext.PixelShader.SetSamplers(0, prevPsSampler); _deviceContext.PixelShader.Set(prevPs); _deviceContext.VertexShader.Set(prevVs); _deviceContext.DomainShader.Set(prevDs); _deviceContext.HullShader.Set(prevHs); _deviceContext.GeometryShader.Set(prevGs); _deviceContext.VertexShader.SetConstantBuffers(0, prevVsConstantBuffer); _deviceContext.InputAssembler.PrimitiveTopology = prevPrimitiveTopology; _deviceContext.InputAssembler.SetIndexBuffer(prevIndexBuffer, prevIndexBufferFormat, prevIndexBufferOffset); _deviceContext.InputAssembler.SetVertexBuffers(0, prevVertexBuffer, prevVertexBufferOffset, prevVertexBufferStride); _deviceContext.InputAssembler.InputLayout = prevInputLayout; } private void SetPerFrameImGuiData(float deltaSeconds) { ImGuiIOPtr io = ImGui.GetIO(); io.DisplaySize = new Vector2(_windowWidth / _scaleFactor.X, _windowHeight / _scaleFactor.Y); io.DisplayFramebufferScale = _scaleFactor; io.DeltaTime = deltaSeconds; // DeltaTime is in seconds. } #endregion private bool _initialized; #region setup [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public bool CreateDeviceObjectsAndFonts() { if (_device == null) return false; if (!_initialized) { if (!CreateShaders()) return false; _initialized = true; } lock (_contextLock) { FontAtlasGenerator.CreateFontAtlasWithIcons(_device, _imguiContext, out _fontTextureView, out _fontSampler); } return true; } private bool CreateShaders() { if (_fontSampler == null) DisposeDeviceObjects(); // Create the vertex shader string vertexShader = @"cbuffer vertexBuffer : register(b0) { float4x4 ProjectionMatrix; }; struct VS_INPUT { float2 pos : POSITION; float4 col : COLOR0; float2 uv : TEXCOORD0; }; struct PS_INPUT { float4 pos : SV_POSITION; float4 col : COLOR0; float2 uv : TEXCOORD0; }; PS_INPUT main(VS_INPUT input) { PS_INPUT output; output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f)); output.col = input.col; output.uv = input.uv; return output; }"; _vertexShaderBlob = ShaderBytecode.Compile(vertexShader, "main", "vs_4_0", ShaderFlags.None, EffectFlags.None); if (_vertexShaderBlob == null) return false; _vertexShader = new VertexShader(_device, _vertexShaderBlob); if (_vertexShader == null) return false; // Create the input layout _inputLayout = new InputLayout(_device, ShaderSignature.GetInputSignature(_vertexShaderBlob), new[] { new InputElement("POSITION", 0, Format.R32G32_Float, 0, 0), new InputElement("TEXCOORD", 0, Format.R32G32_Float, 8, 0), new InputElement("COLOR", 0, Format.R8G8B8A8_UNorm, 16, 0) }); // Create the constant buffer _vertexConstantBuffer = ResourceUtils.CreateDynamicConstantBuffer(_device, Unsafe.SizeOf()); // Create the pixel shader string pixelShader = @"struct PS_INPUT { float4 pos : SV_POSITION; float4 col : COLOR0; float2 uv : TEXCOORD0; }; sampler sampler0; Texture2D texture0; float4 main(PS_INPUT input) : SV_Target { float4 out_col = input.col * texture0.Sample(sampler0, input.uv); return out_col; }"; _pixelShaderBlob = ShaderBytecode.Compile(pixelShader, "main", "ps_4_0", ShaderFlags.None, EffectFlags.None); if (_pixelShaderBlob == null) return false; _pixelShader = new PixelShader(_device, _pixelShaderBlob); if (_pixelShader == null) return false; // Create the blending setup var blendDesc = new BlendStateDescription() { AlphaToCoverageEnable = false, IndependentBlendEnable = false }; blendDesc.RenderTarget[0].IsBlendEnabled = true; blendDesc.RenderTarget[0].SourceBlend = BlendOption.SourceAlpha; blendDesc.RenderTarget[0].DestinationBlend = BlendOption.InverseSourceAlpha; blendDesc.RenderTarget[0].BlendOperation = BlendOperation.Add; blendDesc.RenderTarget[0].SourceAlphaBlend = BlendOption.InverseSourceAlpha; blendDesc.RenderTarget[0].DestinationAlphaBlend = BlendOption.Zero; blendDesc.RenderTarget[0].AlphaBlendOperation = BlendOperation.Add; blendDesc.RenderTarget[0].RenderTargetWriteMask = ColorWriteMaskFlags.All; _blendState = new BlendState(_device, blendDesc); // Create the rasterizer state var rasterizerDesc = new RasterizerStateDescription() { FillMode = FillMode.Solid, CullMode = CullMode.None, IsScissorEnabled = true, IsDepthClipEnabled = true }; _rasterizerState = new RasterizerState(_device, rasterizerDesc); // Create depth-stencil State var depthStencilDesc = new DepthStencilStateDescription() { IsDepthEnabled = false, DepthWriteMask = DepthWriteMask.All, DepthComparison = Comparison.Always, IsStencilEnabled = false }; depthStencilDesc.FrontFace.FailOperation = depthStencilDesc.FrontFace.DepthFailOperation = depthStencilDesc.FrontFace.PassOperation = StencilOperation.Keep; depthStencilDesc.BackFace = depthStencilDesc.FrontFace; _depthStencilState = new DepthStencilState(_device, depthStencilDesc); return true; } public void Dispose() { DisposeDeviceObjects(); } private void DisposeObj(ref T obj) where T : class, IDisposable { obj?.Dispose(); obj = null; } private void DisposeDeviceObjects() { if (_device == null) return; try { // Sadly a resource leak causes this to trigger memory exceptions. // So disabled for now DisposeObj(ref _fontSampler); DisposeObj(ref _fontTextureView); DisposeObj(ref _ib); DisposeObj(ref _vb); DisposeObj(ref _blendState); DisposeObj(ref _depthStencilState); DisposeObj(ref _rasterizerState); DisposeObj(ref _pixelShader); DisposeObj(ref _pixelShaderBlob); DisposeObj(ref _vertexConstantBuffer); DisposeObj(ref _inputLayout); DisposeObj(ref _vertexShader); DisposeObj(ref _vertexShaderBlob); } catch (Exception e) { Log.Warning($"Failed to dispose resources :{e.Message}"); } } #endregion private Device _device; private DeviceContext _deviceContext; private Buffer _vb; private Buffer _ib; private ShaderBytecode _vertexShaderBlob; private VertexShader _vertexShader; private InputLayout _inputLayout; private Buffer _vertexConstantBuffer; private ShaderBytecode _pixelShaderBlob; private PixelShader _pixelShader; private SamplerState _fontSampler; private ShaderResourceView _fontTextureView; private RasterizerState _rasterizerState; private BlendState _blendState; private DepthStencilState _depthStencilState; private int _vertexBufferSize = 5000, _indexBufferSize = 1000; private int _windowWidth; private int _windowHeight; private static ShaderResourceView _viewWindowBackgroundSrv; private readonly Vector2 _scaleFactor = Vector2.One; }