PU-GPU 并行度。在本文的 Demo 里面只会将最后的 WM_Pointer 点绘制出来其 CPU 时间可以忽略降低 CPU-GPU 并行度对此毫无影响再获取IDXGISwapChain2.FrameLatencyWaitableObject可等待对象通过 Win32 的 WaitForSingleObjectEx 方法等待此对象即可获取是个适当的渲染前时机。在此时机将输入进行处理后传给交换链缓存即可获得很低的输入渲染延迟核心代码示例如下var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForXxx(...); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; while (渲染) { Kernal32.WaitForSingleObjectEx(new HANDLE(waitableObject), dwMilliseconds: 1000, bAlertable: true); // 在此编写实际的渲染代码 swapChain2.Present(0, PresentFlags.None); }为什么用WaitForSingleObjectEx(IDXGISwapChain2.FrameLatencyWaitableObject)做等待会比用IDXGISwapChain2.Present(1, ...)的输入响应延迟更低如 官方文档 的下面两张对比图片所示第一张图如下显示的是传统的写法的情况可能让第 5 个数据被延迟到第 5 帧才在屏幕显示出来第二张图如下这是在使用 Windows 8.1 引入的DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT可等待交换链技术的情况下轻松地让输入的响应在第 3 帧渲染出来如上图所示可见采用此技术可能降低输入响应的渲染延迟详细的设计如下让 UI 窗口消息循环线程和 渲染线程 分离在 UI 窗口消息循环接收输入消息如 WM_Pointer 消息。接收到之后将信息进行缓存当 渲染线程 获得渲染时机时取最后一个 WM_Pointer 坐标进行绘制矩形在低延迟的触摸屏设备上运行程序可以尝试触摸移动开启系统触摸反馈点甚至是在触摸过程移动鼠标产生鼠标光标用于对比此方案的输入渲染延迟具体的代码分为三个部分窗口的创建和消息循环对 WM_Pointer 的处理渲染线程的初始化包括初始化 D2D 设备和挂交换链渲染线程每一帧的处理逻辑第一个部分没有什么特殊的可参阅 dotnet DirectX 做一个简单绘制折线笔迹的 D2D 应用 博客了解对 WM_Pointer 消息的处理如果大家对 WM_Pointer 消息感兴趣还请参阅 WPF 从裸 Win 32 的 WM_Pointer 消息获取触摸点绘制笔迹第一部分的代码在这里先简略给出在本文末尾将给出完全的代码和整个项目代码的下载方法[SupportedOSPlatform(windows8.1)] class DemoWindow { public DemoWindow() { var window CreateWindow(); HWND window; // 让鼠标也引发 WM_Pointer 事件 EnableMouseInPointer(true); // 显示窗口 ShowWindow(window, SHOW_WINDOW_CMD.SW_SHOW); } public HWND HWND { get; } public unsafe void Run() { while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } /// summary /// 仅用于防止被回收 /// /summary /// returns/returns private WNDPROC? _wndProcDelegate; private unsafe HWND CreateWindow() { var windowHwnd CreateWindowEx(...); return windowHwnd; } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); ...; var x ...; // 对 pointerInfo.ptHimetricLocationRaw.X 进行处理 var y ...; // 对 pointerInfo.ptHimetricLocationRaw.Y 进行处理 // 通知渲染线程处理 } return DefWindowProc(hwnd, message, wParam, lParam); } }以上是一个标准的窗口的写法。以上代码将被放在 UI 线程执行。再开启另一个线程作为渲染线程渲染线程执行的是第二部分的代码其初始化逻辑前置部分没有什么特殊的按部就班创建交换链。本文这里将使用IDXGIFactory2.CreateSwapChainForHwnd创建交换链。除此之外还可以使用IDXGIFactory2.CreateSwapChainForComposition等方法创建交换链。详细请参阅 Vortice 使用 DirectComposition 显示透明窗口前置代码的核心部分如下可在本文末尾找到全部的代码[SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public HWND HWND hwnd; private void Init() { var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); D3D11.D3D11CreateDevice ( ..., out ID3D11Device d3D11Device, ... ); // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...); ... // 处理交换链的逻辑 } }如对此前置代码的实现原理感兴趣还请参阅 DirectX 使用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色通过前置代码即可拿到 IDXGISwapChain1 交换链。按照上文提供的核心实现方法将 IDXGISwapChain1 转为 IDXGISwapChain2 对象。再设置 MaximumFrameLatency 属性和获取 FrameLatencyWaitableObject 对象IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待将以上的初始化逻辑放在渲染线程里面执行其代码如下[SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public void StartRenderThread() { var thread new Thread(() { RenderCore(); }) { IsBackground true, Name Render }; thread.Priority ThreadPriority.Highest; thread.Start(); } private void RenderCore() { Init(); ... } private void Init() { var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); D3D11.D3D11CreateDevice ( ..., out ID3D11Device d3D11Device, ... ); // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待 } ... }在 RenderCore 还需要对接 D2D 用于渲染其核心代码如下using D2D.ID2D1Factory1 d2DFactory D2D.D2D1.D2D1CreateFactoryD2D.ID2D1Factory1(); var d3D11Texture2D swapChain2.GetBufferID3D11Texture2D(0); using var dxgiSurface d3D11Texture2D.QueryInterfaceIDXGISurface(); D2D.ID2D1RenderTarget d2D1RenderTarget d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, ...);拿到 ID2D1RenderTarget 对象即可在渲染逻辑里面对接渲染第三部分为每一帧执行的逻辑。在 RenderManager 里提供 Move 方法用于接收当前的 Pointer 的坐标点其代码如下public void Move(double x, double y) { _position new Position(x, y); } private Position _position new Position(0, 0); /// summary /// 表示当前的位置 /// /summary /// param nameX/param /// param nameY/param /// remarks /// 为什么需要选用 record 引用 class 类型而不是 struct 结构体值类型这是为了在渲染线程和 UI 线程之间共享这个位置数据。由于 record class 是引用类型所以在两个线程之间共享时不需要担心值类型的复制问题完全原子化不存在多线程安全问题 /// /remarks record Position(double X, double Y);为了更好地测试输入延迟在本文中只考虑 Pointer 的最后一次的坐标点中间点将被覆盖丢弃。由于消息是从 UI 线程接收的而每次渲染都在渲染线程执行为了解决多线程安全问题就将 Position 类型设计为 class 引用类型。这是因为对引用类型的赋值底层是一次指针赋值过程本身就是 CPU 确保的原子化动作不会存在多线程安全问题同步地在消息循环里将处理到的坐标点调用 Move 方法传递到渲染线程class DemoWindow { ... public unsafe void Run() { _renderManager new RenderManager(HWND); _renderManager.StartRenderThread(); while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); ...; var x ...; // 对 pointerInfo.ptHimetricLocationRaw.X 进行处理 var y ...; // 对 pointerInfo.ptHimetricLocationRaw.Y 进行处理 _renderManager?.Move(x, y); } return DefWindowProc(hwnd, message, wParam, lParam); } private RenderManager? _renderManager; }在每一帧的开始先使用Kernal32.WaitForSingleObjectEx等待IDXGISwapChain2.FrameLatencyWaitableObject对象随后再处理输入数据var waitableObject swapChain2.FrameLatencyWaitableObject; using var brush d2D1RenderTarget.CreateSolidColorBrush(Colors.Yellow); while (渲染) { WaitForSingleObjectEx(new HANDLE(waitableObject), dwMilliseconds: 1000, bAlertable: true); // 渲染代码写在这里 D2D.ID2D1RenderTarget renderTarget d2D1RenderTarget; renderTarget.BeginDraw(); renderTarget.Clear(Colors.White); var position _position; // 在输入的坐标上绘制矩形 var rectangleSize 50; renderTarget.FillRectangle(new Rect((float) position.X, (float) position.Y, rectangleSize, rectangleSize), brush); renderTarget.EndDraw(); swapChain2.Present(0, PresentFlags.None); }尝试运行代码最好是脱离 Visual Studio 调试的 Release 版在低延迟触摸屏或高精度鼠标的设备上运行程序可见此应用绘制的矩形是非常跟手的。在触摸屏上尝试打开触摸反馈点设置-辅助功能-鼠标指针与触控-触控指示器-使圆圈更深更大时可见矩形左上角将保持在触摸反馈点中心。如此即可证明渲染的输入响应延迟非常低本文的非 PInvoke 的关键代码全放在 Program.cs 文件里面代码如下using KearjerijarqaloChurharcarwaya.Diagnostics; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using Vortice.DCommon; using Vortice.Direct3D; using Vortice.Direct3D11; using Vortice.DirectComposition; using Vortice.DXGI; using Vortice.Mathematics; using Vortice.Win32; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Gdi; using Windows.Win32.UI.Input.Pointer; using Windows.Win32.UI.WindowsAndMessaging; using static Windows.Win32.PInvoke; using AlphaMode Vortice.DXGI.AlphaMode; using Color Vortice.Mathematics.Color; using D2D Vortice.Direct2D1; namespace KearjerijarqaloChurharcarwaya; class Program { [STAThread] static void Main(string[] args) { if (!OperatingSystem.IsWindowsVersionAtLeast(8, 1)) { return; } var demoWindow new DemoWindow(); demoWindow.Run(); Console.ReadLine(); } } [SupportedOSPlatform(windows8.1)] class DemoWindow { public DemoWindow() { var window CreateWindow(); HWND window; // 让鼠标也引发 WM_Pointer 事件 EnableMouseInPointer(true); // 最大化显示窗口 ShowWindow(window, SHOW_WINDOW_CMD.SW_SHOW); // 独立渲染线程 var renderManager new RenderManager(window); _renderManager renderManager; renderManager.StartRenderThread(); } private readonly RenderManager _renderManager; public HWND HWND { get; } public unsafe void Run() { while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } /// summary /// 仅用于防止被回收 /// /summary /// returns/returns private WNDPROC? _wndProcDelegate; private unsafe HWND CreateWindow() { WINDOW_EX_STYLE exStyle WINDOW_EX_STYLE.WS_EX_APPWINDOW; var style WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW; var defaultCursor LoadCursor( new HINSTANCE(IntPtr.Zero), new PCWSTR(IDC_ARROW.Value)); var className $lindexi-{Guid.NewGuid().ToString()}; var title The Title; fixed (char* pClassName className) fixed (char* pTitle title) { _wndProcDelegate new WNDPROC(WndProc); var wndClassEx new WNDCLASSEXW { cbSize (uint) Marshal.SizeOfWNDCLASSEXW(), style style, lpfnWndProc _wndProcDelegate, hInstance new HINSTANCE(GetModuleHandle(null).DangerousGetHandle()), hCursor defaultCursor, hbrBackground new HBRUSH(IntPtr.Zero), lpszClassName new PCWSTR(pClassName) }; ushort atom RegisterClassEx(in wndClassEx); WINDOW_STYLE dwStyle WINDOW_STYLE.WS_OVERLAPPEDWINDOW | WINDOW_STYLE.WS_VISIBLE | WINDOW_STYLE.WS_CAPTION | WINDOW_STYLE.WS_SYSMENU | WINDOW_STYLE.WS_MINIMIZEBOX | WINDOW_STYLE.WS_CLIPCHILDREN | WINDOW_STYLE.WS_BORDER | WINDOW_STYLE.WS_DLGFRAME | WINDOW_STYLE.WS_THICKFRAME | WINDOW_STYLE.WS_TABSTOP | WINDOW_STYLE.WS_SIZEBOX; var windowHwnd CreateWindowEx( exStyle, new PCWSTR((char*) atom), new PCWSTR(pTitle), dwStyle, 0, 0, 1900, 1000, HWND.Null, HMENU.Null, HINSTANCE.Null, null); return windowHwnd; } } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); global::Windows.Win32.Foundation.RECT pointerDeviceRect default; global::Windows.Win32.Foundation.RECT displayRect default; GetPointerTouchInfo(pointerId, out POINTER_TOUCH_INFO pointerTouchInfo); var pointerInfo pointerTouchInfo.pointerInfo; GetPointerDeviceRects(pointerInfo.sourceDevice, pointerDeviceRect, displayRect); var x pointerInfo.ptHimetricLocationRaw.X / (double) pointerDeviceRect.Width * displayRect.Width displayRect.left; var y pointerInfo.ptHimetricLocationRaw.Y / (double) pointerDeviceRect.Height * displayRect.Height displayRect.top; var screenTranslate new Point(0, 0); ClientToScreen(HWND, ref screenTranslate); x - screenTranslate.X; y - screenTranslate.Y; _renderManager.Move(x, y); } return DefWindowProc(hwnd, message, wParam, lParam); } private static int ToInt32(WPARAM wParam) ToInt32((IntPtr) wParam.Value); private static int ToInt32(IntPtr ptr) IntPtr.Size 4 ? ptr.ToInt32() : (int) (ptr.ToInt64() 0xffffffff); } [SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public HWND HWND hwnd; private readonly Format _colorFormat Format.B8G8R8A8_UNorm; private Format D2DColorFormat _colorFormat; /// summary /// 缓存的数量包括前缓存。大部分应用来说至少需要两个缓存这个玩过游戏的伙伴都知道 /// /summary private const int FrameCount 2; public void StartRenderThread() { var thread new Thread(() { RenderCore(); }) { IsBackground true, Name Render }; thread.Priority ThreadPriority.Highest; thread.Start(); } private void RenderCore() { Init(); using D2D.ID2D1Factory1 d2DFactory D2D.D2D1.D2D1CreateFactoryD2D.ID2D1Factory1(); IDXGISwapChain2 swapChain2 _renderContext.SwapChain; var d3D11Texture2D swapChain2.GetBufferID3D11Texture2D(0); using var dxgiSurface d3D11Texture2D.QueryInterfaceIDXGISurface(); var renderTargetProperties new D2D.RenderTargetProperties() { PixelFormat new PixelFormat(D2DColorFormat, Vortice.DCommon.AlphaMode.Premultiplied), Type D2D.RenderTargetType.Hardware, }; D2D.ID2D1RenderTarget d2D1RenderTarget d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); var waitableObject swapChain2.FrameLatencyWaitableObject; using var brush d2D1RenderTarget.CreateSolidColorBrush(Colors.Yellow); while (!_isDisposed) { using (StepPerformanceCounter.RenderThreadCounter.StepStart(FrameLatencyWaitableObject)) { WaitForSingleObjectEx(new HANDLE(waitableObject), 1000, true); } // 渲染代码写在这里 using (StepPerformanceCounter.RenderThreadCounter.StepStart(Render)) { D2D.ID2D1RenderTarget renderTarget d2D1RenderTarget; renderTarget.BeginDraw(); renderTarget.Clear(Colors.White); var position _position; var rectangleSize 50; renderTarget.FillRectangle(new Rect((float) position.X, (float) position.Y, rectangleSize, rectangleSize), brush); renderTarget.EndDraw(); } using (StepPerformanceCounter.RenderThreadCounter.StepStart(SwapChain)) { swapChain2.Present(0, PresentFlags.None); } } } private void Init() { RECT windowRect; GetClientRect(HWND, windowRect); var clientSize new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top); var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); IDXGIAdapter1? hardwareAdapter GetHardwareAdapter(dxgiFactory2) // 这里 ToList 只是想列出所有的 IDXGIAdapter1 在实际代码里大部分都是获取第一个 .ToList().FirstOrDefault(); if (hardwareAdapter null) { throw new InvalidOperationException(Cannot detect D3D11 adapter); } FeatureLevel[] featureLevels new[] { FeatureLevel.Level_11_1, FeatureLevel.Level_11_0, FeatureLevel.Level_10_1, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3, FeatureLevel.Level_9_2, FeatureLevel.Level_9_1, }; IDXGIAdapter1 adapter hardwareAdapter; DeviceCreationFlags creationFlags DeviceCreationFlags.BgraSupport; var result D3D11.D3D11CreateDevice ( adapter, DriverType.Unknown, creationFlags, featureLevels, out ID3D11Device d3D11Device, out FeatureLevel featureLevel, out ID3D11DeviceContext d3D11DeviceContext ); _ featureLevel; if (result.Failure) { // 如果失败了那就不指定显卡走 WARP 的方式 // http://go.microsoft.com/fwlink/?LinkId286690 result D3D11.D3D11CreateDevice( IntPtr.Zero, DriverType.Warp, creationFlags, featureLevels, out d3D11Device, out featureLevel, out d3D11DeviceContext); // 如果失败就不能继续 result.CheckError(); } // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); var d3D11DeviceContext1 d3D11DeviceContext.QueryInterfaceID3D11DeviceContext1(); // 获取到了新的两个接口就可以减少 d3D11Device 和 d3D11DeviceContext 的引用计数。调用 Dispose 不会释放掉刚才申请的 D3D 资源只是减少引用计数 d3D11Device.Dispose(); d3D11DeviceContext.Dispose(); SwapChainDescription1 swapChainDescription new() { Width (uint) clientSize.Width, Height (uint) clientSize.Height, Format _colorFormat, BufferCount FrameCount, BufferUsage Usage.RenderTargetOutput, SampleDescription SampleDescription.Default, Scaling Scaling.Stretch, SwapEffect SwapEffect.FlipSequential, // 使用 FlipSequential 配合 Composition AlphaMode AlphaMode.Ignore, Flags SwapChainFlags.FrameLatencyWaitableObject, // 核心设置 }; var fullscreenDescription new SwapChainFullscreenDescription() { Windowed true, }; IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, swapChainDescription, fullscreenDescription); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待 // 不要被按下 altenter 进入全屏 dxgiFactory2.MakeWindowAssociation(HWND, WindowAssociationFlags.IgnoreAltEnter | WindowAssociationFlags.IgnorePrintScreen); _renderContext _renderContext with { DXGIFactory2 dxgiFactory2, HardwareAdapter hardwareAdapter, D3D11Device1 d3D11Device1, D3D11DeviceContext1 d3D11DeviceContext1, SwapChain swapChain2, WindowWidth swapChainDescription.Width, WindowHeight swapChainDescription.Height }; } private static IEnumerableIDXGIAdapter1 GetHardwareAdapter(IDXGIFactory2 factory) { using IDXGIFactory6? factory6 factory.QueryInterfaceOrNullIDXGIFactory6(); if (factory6 ! null) { // 这个系统的 DX 支持 IDXGIFactory6 类型 // 先告诉系统要高性能的显卡 for (uint adapterIndex 0; factory6.EnumAdapterByGpuPreference(adapterIndex, GpuPreference.HighPerformance, out IDXGIAdapter1? adapter).Success; adapterIndex) { if (adapter null) { continue; } AdapterDescription1 desc adapter.Description1; if ((desc.Flags AdapterFlags.Software) ! AdapterFlags.None) { // Dont select the Basic Render Driver adapter. adapter.Dispose(); continue; } Console.WriteLine($枚举到 {adapter.Description1.Description} 显卡); yield return adapter; } } else { // 不支持就不支持咯用旧版本的方式获取显示适配器接口 } // 如果枚举不到那系统返回啥都可以 for (uint adapterIndex 0; factory.EnumAdapters1(adapterIndex, out IDXGIAdapter1? adapter).Success; adapterIndex) { AdapterDescription1 desc adapter.Description1; if ((desc.Flags AdapterFlags.Software) ! AdapterFlags.None) { // Dont select the Basic Render Driver adapter. adapter.Dispose(); continue; } Console.WriteLine($枚举到 {adapter.Description1.Description} 显卡); yield return adapter; } } private RenderContext _renderContext;
dotnet DirectX 通过可等待交换链降低输入渲染延迟
发布时间:2026/5/25 8:08:30
PU-GPU 并行度。在本文的 Demo 里面只会将最后的 WM_Pointer 点绘制出来其 CPU 时间可以忽略降低 CPU-GPU 并行度对此毫无影响再获取IDXGISwapChain2.FrameLatencyWaitableObject可等待对象通过 Win32 的 WaitForSingleObjectEx 方法等待此对象即可获取是个适当的渲染前时机。在此时机将输入进行处理后传给交换链缓存即可获得很低的输入渲染延迟核心代码示例如下var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForXxx(...); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; while (渲染) { Kernal32.WaitForSingleObjectEx(new HANDLE(waitableObject), dwMilliseconds: 1000, bAlertable: true); // 在此编写实际的渲染代码 swapChain2.Present(0, PresentFlags.None); }为什么用WaitForSingleObjectEx(IDXGISwapChain2.FrameLatencyWaitableObject)做等待会比用IDXGISwapChain2.Present(1, ...)的输入响应延迟更低如 官方文档 的下面两张对比图片所示第一张图如下显示的是传统的写法的情况可能让第 5 个数据被延迟到第 5 帧才在屏幕显示出来第二张图如下这是在使用 Windows 8.1 引入的DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT可等待交换链技术的情况下轻松地让输入的响应在第 3 帧渲染出来如上图所示可见采用此技术可能降低输入响应的渲染延迟详细的设计如下让 UI 窗口消息循环线程和 渲染线程 分离在 UI 窗口消息循环接收输入消息如 WM_Pointer 消息。接收到之后将信息进行缓存当 渲染线程 获得渲染时机时取最后一个 WM_Pointer 坐标进行绘制矩形在低延迟的触摸屏设备上运行程序可以尝试触摸移动开启系统触摸反馈点甚至是在触摸过程移动鼠标产生鼠标光标用于对比此方案的输入渲染延迟具体的代码分为三个部分窗口的创建和消息循环对 WM_Pointer 的处理渲染线程的初始化包括初始化 D2D 设备和挂交换链渲染线程每一帧的处理逻辑第一个部分没有什么特殊的可参阅 dotnet DirectX 做一个简单绘制折线笔迹的 D2D 应用 博客了解对 WM_Pointer 消息的处理如果大家对 WM_Pointer 消息感兴趣还请参阅 WPF 从裸 Win 32 的 WM_Pointer 消息获取触摸点绘制笔迹第一部分的代码在这里先简略给出在本文末尾将给出完全的代码和整个项目代码的下载方法[SupportedOSPlatform(windows8.1)] class DemoWindow { public DemoWindow() { var window CreateWindow(); HWND window; // 让鼠标也引发 WM_Pointer 事件 EnableMouseInPointer(true); // 显示窗口 ShowWindow(window, SHOW_WINDOW_CMD.SW_SHOW); } public HWND HWND { get; } public unsafe void Run() { while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } /// summary /// 仅用于防止被回收 /// /summary /// returns/returns private WNDPROC? _wndProcDelegate; private unsafe HWND CreateWindow() { var windowHwnd CreateWindowEx(...); return windowHwnd; } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); ...; var x ...; // 对 pointerInfo.ptHimetricLocationRaw.X 进行处理 var y ...; // 对 pointerInfo.ptHimetricLocationRaw.Y 进行处理 // 通知渲染线程处理 } return DefWindowProc(hwnd, message, wParam, lParam); } }以上是一个标准的窗口的写法。以上代码将被放在 UI 线程执行。再开启另一个线程作为渲染线程渲染线程执行的是第二部分的代码其初始化逻辑前置部分没有什么特殊的按部就班创建交换链。本文这里将使用IDXGIFactory2.CreateSwapChainForHwnd创建交换链。除此之外还可以使用IDXGIFactory2.CreateSwapChainForComposition等方法创建交换链。详细请参阅 Vortice 使用 DirectComposition 显示透明窗口前置代码的核心部分如下可在本文末尾找到全部的代码[SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public HWND HWND hwnd; private void Init() { var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); D3D11.D3D11CreateDevice ( ..., out ID3D11Device d3D11Device, ... ); // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...); ... // 处理交换链的逻辑 } }如对此前置代码的实现原理感兴趣还请参阅 DirectX 使用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色通过前置代码即可拿到 IDXGISwapChain1 交换链。按照上文提供的核心实现方法将 IDXGISwapChain1 转为 IDXGISwapChain2 对象。再设置 MaximumFrameLatency 属性和获取 FrameLatencyWaitableObject 对象IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待将以上的初始化逻辑放在渲染线程里面执行其代码如下[SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public void StartRenderThread() { var thread new Thread(() { RenderCore(); }) { IsBackground true, Name Render }; thread.Priority ThreadPriority.Highest; thread.Start(); } private void RenderCore() { Init(); ... } private void Init() { var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); D3D11.D3D11CreateDevice ( ..., out ID3D11Device d3D11Device, ... ); // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待 } ... }在 RenderCore 还需要对接 D2D 用于渲染其核心代码如下using D2D.ID2D1Factory1 d2DFactory D2D.D2D1.D2D1CreateFactoryD2D.ID2D1Factory1(); var d3D11Texture2D swapChain2.GetBufferID3D11Texture2D(0); using var dxgiSurface d3D11Texture2D.QueryInterfaceIDXGISurface(); D2D.ID2D1RenderTarget d2D1RenderTarget d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, ...);拿到 ID2D1RenderTarget 对象即可在渲染逻辑里面对接渲染第三部分为每一帧执行的逻辑。在 RenderManager 里提供 Move 方法用于接收当前的 Pointer 的坐标点其代码如下public void Move(double x, double y) { _position new Position(x, y); } private Position _position new Position(0, 0); /// summary /// 表示当前的位置 /// /summary /// param nameX/param /// param nameY/param /// remarks /// 为什么需要选用 record 引用 class 类型而不是 struct 结构体值类型这是为了在渲染线程和 UI 线程之间共享这个位置数据。由于 record class 是引用类型所以在两个线程之间共享时不需要担心值类型的复制问题完全原子化不存在多线程安全问题 /// /remarks record Position(double X, double Y);为了更好地测试输入延迟在本文中只考虑 Pointer 的最后一次的坐标点中间点将被覆盖丢弃。由于消息是从 UI 线程接收的而每次渲染都在渲染线程执行为了解决多线程安全问题就将 Position 类型设计为 class 引用类型。这是因为对引用类型的赋值底层是一次指针赋值过程本身就是 CPU 确保的原子化动作不会存在多线程安全问题同步地在消息循环里将处理到的坐标点调用 Move 方法传递到渲染线程class DemoWindow { ... public unsafe void Run() { _renderManager new RenderManager(HWND); _renderManager.StartRenderThread(); while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); ...; var x ...; // 对 pointerInfo.ptHimetricLocationRaw.X 进行处理 var y ...; // 对 pointerInfo.ptHimetricLocationRaw.Y 进行处理 _renderManager?.Move(x, y); } return DefWindowProc(hwnd, message, wParam, lParam); } private RenderManager? _renderManager; }在每一帧的开始先使用Kernal32.WaitForSingleObjectEx等待IDXGISwapChain2.FrameLatencyWaitableObject对象随后再处理输入数据var waitableObject swapChain2.FrameLatencyWaitableObject; using var brush d2D1RenderTarget.CreateSolidColorBrush(Colors.Yellow); while (渲染) { WaitForSingleObjectEx(new HANDLE(waitableObject), dwMilliseconds: 1000, bAlertable: true); // 渲染代码写在这里 D2D.ID2D1RenderTarget renderTarget d2D1RenderTarget; renderTarget.BeginDraw(); renderTarget.Clear(Colors.White); var position _position; // 在输入的坐标上绘制矩形 var rectangleSize 50; renderTarget.FillRectangle(new Rect((float) position.X, (float) position.Y, rectangleSize, rectangleSize), brush); renderTarget.EndDraw(); swapChain2.Present(0, PresentFlags.None); }尝试运行代码最好是脱离 Visual Studio 调试的 Release 版在低延迟触摸屏或高精度鼠标的设备上运行程序可见此应用绘制的矩形是非常跟手的。在触摸屏上尝试打开触摸反馈点设置-辅助功能-鼠标指针与触控-触控指示器-使圆圈更深更大时可见矩形左上角将保持在触摸反馈点中心。如此即可证明渲染的输入响应延迟非常低本文的非 PInvoke 的关键代码全放在 Program.cs 文件里面代码如下using KearjerijarqaloChurharcarwaya.Diagnostics; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using Vortice.DCommon; using Vortice.Direct3D; using Vortice.Direct3D11; using Vortice.DirectComposition; using Vortice.DXGI; using Vortice.Mathematics; using Vortice.Win32; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Gdi; using Windows.Win32.UI.Input.Pointer; using Windows.Win32.UI.WindowsAndMessaging; using static Windows.Win32.PInvoke; using AlphaMode Vortice.DXGI.AlphaMode; using Color Vortice.Mathematics.Color; using D2D Vortice.Direct2D1; namespace KearjerijarqaloChurharcarwaya; class Program { [STAThread] static void Main(string[] args) { if (!OperatingSystem.IsWindowsVersionAtLeast(8, 1)) { return; } var demoWindow new DemoWindow(); demoWindow.Run(); Console.ReadLine(); } } [SupportedOSPlatform(windows8.1)] class DemoWindow { public DemoWindow() { var window CreateWindow(); HWND window; // 让鼠标也引发 WM_Pointer 事件 EnableMouseInPointer(true); // 最大化显示窗口 ShowWindow(window, SHOW_WINDOW_CMD.SW_SHOW); // 独立渲染线程 var renderManager new RenderManager(window); _renderManager renderManager; renderManager.StartRenderThread(); } private readonly RenderManager _renderManager; public HWND HWND { get; } public unsafe void Run() { while (true) { var msg new MSG(); var getMessageResult GetMessage(msg, HWND, 0, 0); if (!getMessageResult) { break; } TranslateMessage(msg); DispatchMessage(msg); } } /// summary /// 仅用于防止被回收 /// /summary /// returns/returns private WNDPROC? _wndProcDelegate; private unsafe HWND CreateWindow() { WINDOW_EX_STYLE exStyle WINDOW_EX_STYLE.WS_EX_APPWINDOW; var style WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW; var defaultCursor LoadCursor( new HINSTANCE(IntPtr.Zero), new PCWSTR(IDC_ARROW.Value)); var className $lindexi-{Guid.NewGuid().ToString()}; var title The Title; fixed (char* pClassName className) fixed (char* pTitle title) { _wndProcDelegate new WNDPROC(WndProc); var wndClassEx new WNDCLASSEXW { cbSize (uint) Marshal.SizeOfWNDCLASSEXW(), style style, lpfnWndProc _wndProcDelegate, hInstance new HINSTANCE(GetModuleHandle(null).DangerousGetHandle()), hCursor defaultCursor, hbrBackground new HBRUSH(IntPtr.Zero), lpszClassName new PCWSTR(pClassName) }; ushort atom RegisterClassEx(in wndClassEx); WINDOW_STYLE dwStyle WINDOW_STYLE.WS_OVERLAPPEDWINDOW | WINDOW_STYLE.WS_VISIBLE | WINDOW_STYLE.WS_CAPTION | WINDOW_STYLE.WS_SYSMENU | WINDOW_STYLE.WS_MINIMIZEBOX | WINDOW_STYLE.WS_CLIPCHILDREN | WINDOW_STYLE.WS_BORDER | WINDOW_STYLE.WS_DLGFRAME | WINDOW_STYLE.WS_THICKFRAME | WINDOW_STYLE.WS_TABSTOP | WINDOW_STYLE.WS_SIZEBOX; var windowHwnd CreateWindowEx( exStyle, new PCWSTR((char*) atom), new PCWSTR(pTitle), dwStyle, 0, 0, 1900, 1000, HWND.Null, HMENU.Null, HINSTANCE.Null, null); return windowHwnd; } } private unsafe LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message WM_POINTERUPDATE /*Pointer Update*/) { var pointerId (uint) (ToInt32(wParam) 0xFFFF); global::Windows.Win32.Foundation.RECT pointerDeviceRect default; global::Windows.Win32.Foundation.RECT displayRect default; GetPointerTouchInfo(pointerId, out POINTER_TOUCH_INFO pointerTouchInfo); var pointerInfo pointerTouchInfo.pointerInfo; GetPointerDeviceRects(pointerInfo.sourceDevice, pointerDeviceRect, displayRect); var x pointerInfo.ptHimetricLocationRaw.X / (double) pointerDeviceRect.Width * displayRect.Width displayRect.left; var y pointerInfo.ptHimetricLocationRaw.Y / (double) pointerDeviceRect.Height * displayRect.Height displayRect.top; var screenTranslate new Point(0, 0); ClientToScreen(HWND, ref screenTranslate); x - screenTranslate.X; y - screenTranslate.Y; _renderManager.Move(x, y); } return DefWindowProc(hwnd, message, wParam, lParam); } private static int ToInt32(WPARAM wParam) ToInt32((IntPtr) wParam.Value); private static int ToInt32(IntPtr ptr) IntPtr.Size 4 ? ptr.ToInt32() : (int) (ptr.ToInt64() 0xffffffff); } [SupportedOSPlatform(windows8.1)] unsafe class RenderManager(HWND hwnd) : IDisposable { public HWND HWND hwnd; private readonly Format _colorFormat Format.B8G8R8A8_UNorm; private Format D2DColorFormat _colorFormat; /// summary /// 缓存的数量包括前缓存。大部分应用来说至少需要两个缓存这个玩过游戏的伙伴都知道 /// /summary private const int FrameCount 2; public void StartRenderThread() { var thread new Thread(() { RenderCore(); }) { IsBackground true, Name Render }; thread.Priority ThreadPriority.Highest; thread.Start(); } private void RenderCore() { Init(); using D2D.ID2D1Factory1 d2DFactory D2D.D2D1.D2D1CreateFactoryD2D.ID2D1Factory1(); IDXGISwapChain2 swapChain2 _renderContext.SwapChain; var d3D11Texture2D swapChain2.GetBufferID3D11Texture2D(0); using var dxgiSurface d3D11Texture2D.QueryInterfaceIDXGISurface(); var renderTargetProperties new D2D.RenderTargetProperties() { PixelFormat new PixelFormat(D2DColorFormat, Vortice.DCommon.AlphaMode.Premultiplied), Type D2D.RenderTargetType.Hardware, }; D2D.ID2D1RenderTarget d2D1RenderTarget d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); var waitableObject swapChain2.FrameLatencyWaitableObject; using var brush d2D1RenderTarget.CreateSolidColorBrush(Colors.Yellow); while (!_isDisposed) { using (StepPerformanceCounter.RenderThreadCounter.StepStart(FrameLatencyWaitableObject)) { WaitForSingleObjectEx(new HANDLE(waitableObject), 1000, true); } // 渲染代码写在这里 using (StepPerformanceCounter.RenderThreadCounter.StepStart(Render)) { D2D.ID2D1RenderTarget renderTarget d2D1RenderTarget; renderTarget.BeginDraw(); renderTarget.Clear(Colors.White); var position _position; var rectangleSize 50; renderTarget.FillRectangle(new Rect((float) position.X, (float) position.Y, rectangleSize, rectangleSize), brush); renderTarget.EndDraw(); } using (StepPerformanceCounter.RenderThreadCounter.StepStart(SwapChain)) { swapChain2.Present(0, PresentFlags.None); } } } private void Init() { RECT windowRect; GetClientRect(HWND, windowRect); var clientSize new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top); var dxgiFactory2 DXGI.CreateDXGIFactory1IDXGIFactory2(); IDXGIAdapter1? hardwareAdapter GetHardwareAdapter(dxgiFactory2) // 这里 ToList 只是想列出所有的 IDXGIAdapter1 在实际代码里大部分都是获取第一个 .ToList().FirstOrDefault(); if (hardwareAdapter null) { throw new InvalidOperationException(Cannot detect D3D11 adapter); } FeatureLevel[] featureLevels new[] { FeatureLevel.Level_11_1, FeatureLevel.Level_11_0, FeatureLevel.Level_10_1, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3, FeatureLevel.Level_9_2, FeatureLevel.Level_9_1, }; IDXGIAdapter1 adapter hardwareAdapter; DeviceCreationFlags creationFlags DeviceCreationFlags.BgraSupport; var result D3D11.D3D11CreateDevice ( adapter, DriverType.Unknown, creationFlags, featureLevels, out ID3D11Device d3D11Device, out FeatureLevel featureLevel, out ID3D11DeviceContext d3D11DeviceContext ); _ featureLevel; if (result.Failure) { // 如果失败了那就不指定显卡走 WARP 的方式 // http://go.microsoft.com/fwlink/?LinkId286690 result D3D11.D3D11CreateDevice( IntPtr.Zero, DriverType.Warp, creationFlags, featureLevels, out d3D11Device, out featureLevel, out d3D11DeviceContext); // 如果失败就不能继续 result.CheckError(); } // 大部分情况下用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型 // 从 ID3D11Device 转换为 ID3D11Device1 类型 ID3D11Device1 d3D11Device1 d3D11Device.QueryInterfaceID3D11Device1(); var d3D11DeviceContext1 d3D11DeviceContext.QueryInterfaceID3D11DeviceContext1(); // 获取到了新的两个接口就可以减少 d3D11Device 和 d3D11DeviceContext 的引用计数。调用 Dispose 不会释放掉刚才申请的 D3D 资源只是减少引用计数 d3D11Device.Dispose(); d3D11DeviceContext.Dispose(); SwapChainDescription1 swapChainDescription new() { Width (uint) clientSize.Width, Height (uint) clientSize.Height, Format _colorFormat, BufferCount FrameCount, BufferUsage Usage.RenderTargetOutput, SampleDescription SampleDescription.Default, Scaling Scaling.Stretch, SwapEffect SwapEffect.FlipSequential, // 使用 FlipSequential 配合 Composition AlphaMode AlphaMode.Ignore, Flags SwapChainFlags.FrameLatencyWaitableObject, // 核心设置 }; var fullscreenDescription new SwapChainFullscreenDescription() { Windowed true, }; IDXGISwapChain1 swapChain1 dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, swapChainDescription, fullscreenDescription); IDXGISwapChain2 swapChain2 swapChain1.QueryInterfaceIDXGISwapChain2(); swapChain1.Dispose(); swapChain2.MaximumFrameLatency 1; var waitableObject swapChain2.FrameLatencyWaitableObject; _ waitableObject; // 可以通过 WaitForSingleObjectEx 进行等待 // 不要被按下 altenter 进入全屏 dxgiFactory2.MakeWindowAssociation(HWND, WindowAssociationFlags.IgnoreAltEnter | WindowAssociationFlags.IgnorePrintScreen); _renderContext _renderContext with { DXGIFactory2 dxgiFactory2, HardwareAdapter hardwareAdapter, D3D11Device1 d3D11Device1, D3D11DeviceContext1 d3D11DeviceContext1, SwapChain swapChain2, WindowWidth swapChainDescription.Width, WindowHeight swapChainDescription.Height }; } private static IEnumerableIDXGIAdapter1 GetHardwareAdapter(IDXGIFactory2 factory) { using IDXGIFactory6? factory6 factory.QueryInterfaceOrNullIDXGIFactory6(); if (factory6 ! null) { // 这个系统的 DX 支持 IDXGIFactory6 类型 // 先告诉系统要高性能的显卡 for (uint adapterIndex 0; factory6.EnumAdapterByGpuPreference(adapterIndex, GpuPreference.HighPerformance, out IDXGIAdapter1? adapter).Success; adapterIndex) { if (adapter null) { continue; } AdapterDescription1 desc adapter.Description1; if ((desc.Flags AdapterFlags.Software) ! AdapterFlags.None) { // Dont select the Basic Render Driver adapter. adapter.Dispose(); continue; } Console.WriteLine($枚举到 {adapter.Description1.Description} 显卡); yield return adapter; } } else { // 不支持就不支持咯用旧版本的方式获取显示适配器接口 } // 如果枚举不到那系统返回啥都可以 for (uint adapterIndex 0; factory.EnumAdapters1(adapterIndex, out IDXGIAdapter1? adapter).Success; adapterIndex) { AdapterDescription1 desc adapter.Description1; if ((desc.Flags AdapterFlags.Software) ! AdapterFlags.None) { // Dont select the Basic Render Driver adapter. adapter.Dispose(); continue; } Console.WriteLine($枚举到 {adapter.Description1.Description} 显卡); yield return adapter; } } private RenderContext _renderContext;