如何通过可等待交换链降低dotnet DirectX输入渲染延迟?
摘要:在 DXGI 1.3 引入了新的功能,支持获得交换链发出开始渲染新帧的适当时机信号,通过等待此信号,可以降低输入的渲染延迟
在 上一篇博客 和大家介绍了如何在控制台里面用裸 DirectX 做一个简单绘制折线笔迹的 D2D 应用。此时的 D2D 应用的笔迹延迟还只是能够追得上 WPF 的笔迹性能,依然有很大的优化空间。本文将在此基础上,给出一个更低输入延迟的渲染方案
在一些紧张的射击类游戏里面,游戏开发者很注重于减少输入的渲染延迟。对桌面应用来说,也有很多领域有着相同的追求。比如笔迹类白板应用。这些应用都追求着尽快将用户的输入内容呈现在屏幕上
对于桌面类应用来说,有一个很讽刺的点在于,如果一个应用程序的一帧渲染时间足够短,那渲染线程很大的时间都是在等待交换链进行同步过程中。在等待的过程中,此时的 DWM 桌面窗口合成器还没能将窗口画面送出去渲染,在这段时间内的所有输入内容都将会被延迟到下一帧进行处理,甚至是下下帧进行处理
这就是著名的 Input latency (输入延迟)问题。解决此问题的方向有很多,在本文这里将和大家介绍的是在 Windows 8.1 中的 DXGI 1.3 版本引入的可等待交换链技术
本文属于 DirectX 系列博客,更多 DirectX 相关博客,请参阅 博客导航
在开始之前,我十分推荐大家先阅读 分享一个在 dotnet 里使用 D2D 配合 AOT 开发小而美的应用开发经验 这篇博客,通过阅读此博客,可以让大家理解一些常用概念
核心使用可等待交换链的代码很少,只需将从通过 IDXGIFactory2.CreateSwapChainForXxx 获得的 IDXGISwapChain1 当成 IDXGISwapChain2 对象,再设置 MaximumFrameLatency 为 1 的值,表示实现最低延迟,但其代价是降低 CPU-GPU 并行度。在本文的 Demo 里面,只会将最后的 WM_Pointer 点绘制出来,其 CPU 时间可以忽略,降低 CPU-GPU 并行度对此毫无影响
再获取 IDXGISwapChain2.FrameLatencyWaitableObject 可等待对象,通过 Win32 的 WaitForSingleObjectEx 方法等待此对象,即可获取是个适当的渲染前时机。在此时机将输入进行处理后传给交换链缓存即可获得很低的输入渲染延迟
核心代码示例如下:
var dxgiFactory2 = DXGI.CreateDXGIFactory1<IDXGIFactory2>();
IDXGISwapChain1 swapChain1 = dxgiFactory2.CreateSwapChainForXxx(...);
IDXGISwapChain2 swapChain2 = swapChain1.QueryInterface<IDXGISwapChain2>();
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 消息。
