using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Windows.Forms; using SharpDX; using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; using SharpDX.Windows; using T3.Core.DataTypes.Vector; using T3.Core.Resource; using T3.Core.SystemUi; using T3.Editor.Gui.Styling; using Device = SharpDX.Direct3D11.Device; using Icon = System.Drawing.Icon; using Rectangle = System.Drawing.Rectangle; using Resource = SharpDX.Direct3D11.Resource; using Vector2 = System.Numerics.Vector2; namespace T3.Editor.App; /// /// Functions and properties related to rendering DX11 content into RenderForm windows /// internal sealed class AppWindow { public IntPtr HwndHandle => Form.Handle; public Int2 Size => new(Width, Height); public int Width => Form.ClientSize.Width; public int Height => Form.ClientSize.Height; public bool IsFullScreen => Form.FormBorderStyle == FormBorderStyle.None; internal SwapChain SwapChain { get => _swapChain; private set => _swapChain = value; } internal RenderTargetView RenderTargetView { get => _renderTargetView; private set => _renderTargetView = value; } internal ImGuiDx11RenderForm Form { get; private set; } /// /// Set before swap-chain creation. When true, the swap chain is created with /// ; the latency handle is captured /// after creation and can be called once per frame as the /// pacing primitive — the proper replacement for the accidental pacing that was happening /// via the secondary Viewer's `Present(1)` call. /// internal bool UseFrameLatencyWaitable { get; set; } internal SwapChainDescription SwapChainDescription => new() { ModeDescription = new ModeDescription(Width, Height, new Rational(60, 1), Format.R8G8B8A8_UNorm), IsWindowed = true, OutputHandle = Form.Handle, SampleDescription = new SampleDescription(1, 0), BufferCount = 2, SwapEffect = SwapEffect.FlipDiscard, Usage = Usage.RenderTargetOutput, Flags = UseFrameLatencyWaitable ? SwapChainFlags.FrameLatencyWaitAbleObject : SwapChainFlags.None, }; internal bool IsMinimized => Form.WindowState == FormWindowState.Minimized; internal bool IsCursorOverWindow => Form.Bounds.Contains(CoreUi.Instance.Cursor.Position); public Texture2D Texture { get; set; } internal AppWindow(string windowTitle, bool disableClose) { CreateRenderForm(windowTitle, disableClose); } public void SetVisible(bool isVisible) { // Form can be disposed mid-frame (shutdown / display reconfigure); // Visible= on a disposed Form throws via CreateHandle. if (Form.IsDisposed) return; Form.Visible = isVisible; } public void SetSizeable() { Form.FormBorderStyle = FormBorderStyle.Sizable; if (_boundsBeforeFullscreen.Height != 0 && _boundsBeforeFullscreen.Width != 0) { Form.Bounds = _boundsBeforeFullscreen; } } public void Show() => Form.Show(); public Vector2 GetDpi() { using Graphics graphics = Form.CreateGraphics(); Vector2 dpi = new(graphics.DpiX, graphics.DpiY); return dpi; } internal void SetFullScreen(int screenIndex) { _boundsBeforeFullscreen = Form.Bounds; Form.FormBorderStyle = FormBorderStyle.Sizable; Form.WindowState = FormWindowState.Normal; Form.FormBorderStyle = FormBorderStyle.None; Form.Bounds = Screen.AllScreens[screenIndex].Bounds; } internal void UpdateSpanningBounds(int x, int y, int width, int height) { if (Form.FormBorderStyle == FormBorderStyle.None) { Form.Bounds = new Rectangle(x, y, width, height); } else { _boundsBeforeFullscreen = Form.Bounds; Form.FormBorderStyle = FormBorderStyle.None; Form.Bounds = new Rectangle(x, y, width, height); } } internal void InitViewSwapChain(Factory factory) { SwapChain = new SwapChain(factory, _device, SwapChainDescription); SwapChain.ResizeBuffers(bufferCount: 3, Width, Height, SwapChain.Description.ModeDescription.Format, SwapChain.Description.Flags); CaptureFrameLatencyWaitableHandleIfEnabled(); } internal void PrepareRenderingFrame() { _deviceContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; _deviceContext.Rasterizer.SetViewport(new Viewport(0, 0, Width, Height, 0.0f, 1.0f)); _deviceContext.OutputMerger.SetTargets(RenderTargetView); var color = UiColors.WindowBackground.ToByte4(); var sharpDxColor = new SharpDX.Color(color.X, color.Y, color.Z, color.W); _deviceContext.ClearRenderTargetView(RenderTargetView, sharpDxColor); } internal void RunRenderLoop(Action callback) { _renderCallback = callback; RenderLoop.Run(Form, () => callback()); } internal void SetSize(int width, int height) => Form.ClientSize = new Size(width, height); internal void SetBorderStyleSizable() => Form.FormBorderStyle = FormBorderStyle.Sizable; internal void InitializeWindow(FormWindowState windowState, FormClosingEventHandler handleClose, bool handleKeys) { InitRenderTargetsAndEventHandlers(); if (handleKeys) { MsForms.MsForms.TrackKeysOf(Form); } MsForms.MsForms.TrackMouseOf(Form); if (handleClose != null) Form.FormClosing += handleClose; Form.WindowState = windowState; } internal void SetDevice(Device device, DeviceContext deviceContext, SwapChain swapChain = null) { if (_hasSetDevice) throw new InvalidOperationException("Device has already been set"); _hasSetDevice = true; _device = device; _deviceContext = deviceContext; _swapChain = swapChain; CaptureFrameLatencyWaitableHandleIfEnabled(); } /// /// Captures the swap chain's frame-latency waitable handle (if the chain was created with the /// matching flag) and sets a low maximum frame latency. The handle must then be waited on once /// per frame via as the loop's pacing primitive. /// private void CaptureFrameLatencyWaitableHandleIfEnabled() { if (!UseFrameLatencyWaitable || _swapChain == null) return; try { using var swapChain2 = _swapChain.QueryInterface(); _frameLatencyWaitableHandle = swapChain2.FrameLatencyWaitableObject; // 1 = lowest latency, 2 = small queue depth (less risk of stalls under jitter). swapChain2.MaximumFrameLatency = 2; } catch (SharpDX.SharpDXException e) { // QueryInterface or property access can fail on very old DXGI runtimes. // Falls back to no-pacing — frame still works, just no waitable benefit. Log.Warning($"Could not enable FrameLatencyWaitableObject pacing: {e.Message}"); _frameLatencyWaitableHandle = IntPtr.Zero; } } /// /// Block until DXGI signals "you may submit the next frame." Should be called once at the very /// start of each frame iteration. No-op if wasn't enabled /// before swap-chain creation, or if the handle couldn't be obtained. /// public void WaitForFrameLatency() { if (_frameLatencyWaitableHandle == IntPtr.Zero) return; WaitForSingleObjectEx(_frameLatencyWaitableHandle, 1000, false); } [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObjectEx(IntPtr hHandle, uint dwMilliseconds, bool bAlertable); private IntPtr _frameLatencyWaitableHandle; internal void Release() { _renderTargetView.Dispose(); _backBufferTexture.Dispose(); _swapChain.Dispose(); } private void CreateRenderForm(string windowTitle, bool disableClose) { var fileName = Path.Combine(SharedResources.EditorResourcesDirectory, SharedResources.EditorResourcesDirectory, "images", "t3.ico"); Form = disableClose ? new NoCloseRenderForm(windowTitle) { ClientSize = new Size(640, 360 + 20), Icon = new Icon(fileName, 48, 48), FormBorderStyle = FormBorderStyle.None, } : new ImGuiDx11RenderForm(windowTitle) { ClientSize = new Size(640, 480), Icon = new Icon(fileName, 48, 48) }; } private void InitRenderTargetsAndEventHandlers() { var device = _device; _backBufferTexture = Resource.FromSwapChain(SwapChain, 0); RenderTargetView = new RenderTargetView(device, _backBufferTexture); Form.ResizeBegin += (sender, args) => _isResizingRightNow = true; Form.ResizeEnd += (sender, args) => _isResizingRightNow = false; Form.ClientSizeChanged += (sender, args) => { if (Form.ClientSize.Width == 0 || Form.ClientSize.Height == 0) return; RebuildBackBuffer(Form, device, ref _renderTargetView, ref _backBufferTexture, ref _swapChain); if (_isResizingRightNow) _renderCallback(); }; } private static void RebuildBackBuffer(Form form, Device device, ref RenderTargetView rtv, ref Texture2D buffer, ref SwapChain swapChain) { rtv.Dispose(); buffer.Dispose(); // Preserve the swap chain's existing flags (in particular FrameLatencyWaitableObject must // be carried across resize, otherwise the waitable handle becomes invalid). swapChain.ResizeBuffers(3, form.ClientSize.Width, form.ClientSize.Height, Format.Unknown, swapChain.Description.Flags); buffer = Resource.FromSwapChain(swapChain, 0); rtv = new RenderTargetView(device, buffer); } /// /// We prevent closing the secondary viewer window for now because /// this will cause a SwapChain related crash /// private sealed class NoCloseRenderForm : ImGuiDx11RenderForm { private const int CpNocloseButton = 0x200; protected override CreateParams CreateParams { get { CreateParams myCp = base.CreateParams; myCp.ClassStyle = myCp.ClassStyle | CpNocloseButton; return myCp; } } public NoCloseRenderForm(string title) : base(title) { } } private bool _hasSetDevice; private Device _device; private DeviceContext _deviceContext; private SwapChain _swapChain; private RenderTargetView _renderTargetView; private Texture2D _backBufferTexture; public Texture2D BackBufferTexture => _backBufferTexture; private bool _isResizingRightNow; private Action _renderCallback; private Rectangle _boundsBeforeFullscreen; public void SetTexture(Texture2D texture) { Texture = texture; } }