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

316 lines
13 KiB
C#

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;
/// <summary>
/// Functions and properties related to rendering DX11 content into RenderForm windows
/// </summary>
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; }
/// <summary>
/// Set before swap-chain creation. When true, the swap chain is created with
/// <see cref="SwapChainFlags.FrameLatencyWaitAbleObject"/>; the latency handle is captured
/// after creation and <see cref="WaitForFrameLatency"/> 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.
/// </summary>
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();
}
/// <summary>
/// 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 <see cref="WaitForFrameLatency"/> as the loop's pacing primitive.
/// </summary>
private void CaptureFrameLatencyWaitableHandleIfEnabled()
{
if (!UseFrameLatencyWaitable || _swapChain == null)
return;
try
{
using var swapChain2 = _swapChain.QueryInterface<SwapChain2>();
_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;
}
}
/// <summary>
/// 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 <see cref="UseFrameLatencyWaitable"/> wasn't enabled
/// before swap-chain creation, or if the handle couldn't be obtained.
/// </summary>
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<Texture2D>(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<Texture2D>(swapChain, 0);
rtv = new RenderTargetView(device, buffer);
}
/// <summary>
/// We prevent closing the secondary viewer window for now because
/// this will cause a SwapChain related crash
/// </summary>
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;
}
}