Files
2026-07-13 13:13:17 +08:00

630 lines
27 KiB
C#

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;
/// <summary>
/// Renders draw data generated by ImGui with SharpDX render form
/// </summary>
internal sealed class WindowsUiContentDrawer : IUiContentDrawer<Device>
{
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<ImDrawVert>(),
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<ushort>(),
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<ImDrawVert>()));
ibStream.WriteRange(cmdList.IdxBuffer.Data, (uint)(cmdList.IdxBuffer.Size * Unsafe.SizeOf<ushort>()));
}
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<Matrix4x4>(_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<ImDrawVert>();
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<Matrix4x4>());
// 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<T>(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;
}