在 上一篇博客 和大家介绍了如何在控制台里面用裸 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 方法等待此对象,即可获取是个适当的渲染前时机。在此时机将输入进行处理后传给交换链缓存即可获得很低的输入渲染延迟
核心代码示例如下:
csharp
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 消息。接收到之后,将信息进行缓存
- 当 渲染线程 获得渲染时机时,取最后一个 WM_Pointer 坐标进行绘制矩形
在低延迟的触摸屏设备上运行程序,可以尝试触摸移动,开启系统触摸反馈点,甚至是在触摸过程移动鼠标产生鼠标光标,用于对比此方案的输入渲染延迟
具体的代码分为三个部分:
- 窗口的创建和消息循环对 WM_Pointer 的处理
- 渲染线程的初始化,包括初始化 D2D 设备和挂交换链
- 渲染线程每一帧的处理逻辑
第一个部分没有什么特殊的,可参阅 dotnet DirectX 做一个简单绘制折线笔迹的 D2D 应用 博客了解对 WM_Pointer 消息的处理
如果大家对 WM_Pointer 消息感兴趣,还请参阅 WPF 从裸 Win 32 的 WM_Pointer 消息获取触摸点绘制笔迹
第一部分的代码在这里先简略给出,在本文末尾将给出完全的代码,和整个项目代码的下载方法
csharp
[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 显示透明窗口
前置代码的核心部分如下,可在本文末尾找到全部的代码
csharp
[SupportedOSPlatform("windows8.1")]
unsafe class RenderManager(HWND hwnd) : IDisposable
{
public HWND HWND => hwnd;
private void Init()
{
var dxgiFactory2 = DXGI.CreateDXGIFactory1<IDXGIFactory2>();
D3D11.D3D11CreateDevice
(
...,
out ID3D11Device d3D11Device,
...
);
// 大部分情况下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型
// 从 ID3D11Device 转换为 ID3D11Device1 类型
ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
IDXGISwapChain1 swapChain1 = dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...);
... // 处理交换链的逻辑
}
}
如对此前置代码的实现原理感兴趣,还请参阅 DirectX 使用 Vortice 从零开始控制台创建 Direct2D1 窗口修改颜色
通过前置代码即可拿到 IDXGISwapChain1 交换链。按照上文提供的核心实现方法,将 IDXGISwapChain1 转为 IDXGISwapChain2 对象。再设置 MaximumFrameLatency 属性和获取 FrameLatencyWaitableObject 对象
csharp
IDXGISwapChain2 swapChain2 = swapChain1.QueryInterface<IDXGISwapChain2>();
swapChain1.Dispose();
swapChain2.MaximumFrameLatency = 1;
var waitableObject = swapChain2.FrameLatencyWaitableObject;
_ = waitableObject;
// 可以通过 WaitForSingleObjectEx 进行等待
将以上的初始化逻辑放在渲染线程里面执行,其代码如下
csharp
[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.CreateDXGIFactory1<IDXGIFactory2>();
D3D11.D3D11CreateDevice
(
...,
out ID3D11Device d3D11Device,
...
);
// 大部分情况下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型
// 从 ID3D11Device 转换为 ID3D11Device1 类型
ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
IDXGISwapChain1 swapChain1 = dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, HWND, ...);
IDXGISwapChain2 swapChain2 = swapChain1.QueryInterface<IDXGISwapChain2>();
swapChain1.Dispose();
swapChain2.MaximumFrameLatency = 1;
var waitableObject = swapChain2.FrameLatencyWaitableObject;
_ = waitableObject;
// 可以通过 WaitForSingleObjectEx 进行等待
}
...
}
在 RenderCore 还需要对接 D2D 用于渲染,其核心代码如下
csharp
using D2D.ID2D1Factory1 d2DFactory = D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();
var d3D11Texture2D = swapChain2.GetBuffer<ID3D11Texture2D>(0);
using var dxgiSurface = d3D11Texture2D.QueryInterface<IDXGISurface>();
D2D.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, ...);
拿到 ID2D1RenderTarget 对象即可在渲染逻辑里面对接渲染
第三部分为每一帧执行的逻辑。在 RenderManager 里提供 Move 方法,用于接收当前的 Pointer 的坐标点,其代码如下
csharp
public void Move(double x, double y)
{
_position = new Position(x, y);
}
private Position _position = new Position(0, 0);
/// <summary>
/// 表示当前的位置
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
/// <remarks>
/// 为什么需要选用 record 引用 class 类型,而不是 struct 结构体值类型?这是为了在渲染线程和 UI 线程之间共享这个位置数据。由于 record class 是引用类型,所以在两个线程之间共享时,不需要担心值类型的复制问题,完全原子化,不存在多线程安全问题
/// </remarks>
record Position(double X, double Y);
为了更好地测试输入延迟,在本文中只考虑 Pointer 的最后一次的坐标点,中间点将被覆盖丢弃。由于消息是从 UI 线程接收的,而每次渲染都在渲染线程执行,为了解决多线程安全问题,就将 Position 类型设计为 class 引用类型。这是因为对引用类型的赋值底层是一次指针赋值过程,本身就是 CPU 确保的原子化动作,不会存在多线程安全问题
同步地在消息循环里将处理到的坐标点调用 Move 方法传递到渲染线程
csharp
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 对象,随后再处理输入数据
csharp
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 文件里面,代码如下
csharp
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.SizeOf<WNDCLASSEXW>(),
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.D2D1CreateFactory<D2D.ID2D1Factory1>();
IDXGISwapChain2 swapChain2 = _renderContext.SwapChain;
var d3D11Texture2D = swapChain2.GetBuffer<ID3D11Texture2D>(0);
using var dxgiSurface = d3D11Texture2D.QueryInterface<IDXGISurface>();
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.CreateDXGIFactory1<IDXGIFactory2>();
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/?LinkId=286690
result = D3D11.D3D11CreateDevice(
IntPtr.Zero,
DriverType.Warp,
creationFlags,
featureLevels,
out d3D11Device, out featureLevel, out d3D11DeviceContext);
// 如果失败,就不能继续
result.CheckError();
}
// 大部分情况下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型
// 从 ID3D11Device 转换为 ID3D11Device1 类型
ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<ID3D11DeviceContext1>();
// 获取到了新的两个接口,就可以减少 `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.QueryInterface<IDXGISwapChain2>();
swapChain1.Dispose();
swapChain2.MaximumFrameLatency = 1;
var waitableObject = swapChain2.FrameLatencyWaitableObject;
_ = waitableObject;
// 可以通过 WaitForSingleObjectEx 进行等待
// 不要被按下 alt+enter 进入全屏
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 IEnumerable<IDXGIAdapter1> GetHardwareAdapter(IDXGIFactory2 factory)
{
using IDXGIFactory6? factory6 = factory.QueryInterfaceOrNull<IDXGIFactory6>();
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)
{
// Don't 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)
{
// Don't select the Basic Render Driver adapter.
adapter.Dispose();
continue;
}
Console.WriteLine($"枚举到 {adapter.Description1.Description} 显卡");
yield return adapter;
}
}
private RenderContext _renderContext;
public void Dispose()
{
_renderContext.Dispose();
_isDisposed = true;
}
private bool _isDisposed;
public void Move(double x, double y)
{
_position = new Position(x, y);
}
private Position _position = new Position(0, 0);
/// <summary>
/// 表示当前的位置
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
/// <remarks>
/// 为什么需要选用 record 引用 class 类型,而不是 struct 结构体值类型?这是为了在渲染线程和 UI 线程之间共享这个位置数据。由于 record class 是引用类型,所以在两个线程之间共享时,不需要担心值类型的复制问题,完全原子化,不存在多线程安全问题
/// </remarks>
record Position(double X, double Y);
}
readonly record struct RenderContext(
IDXGIFactory2 DXGIFactory2,
IDXGIAdapter1 HardwareAdapter,
ID3D11Device1 D3D11Device1,
ID3D11DeviceContext1 D3D11DeviceContext1,
IDXGISwapChain2 SwapChain) : IDisposable
{
public uint WindowWidth { get; init; }
public uint WindowHeight { get; init; }
public void Dispose()
{
DXGIFactory2.Dispose();
HardwareAdapter.Dispose();
D3D11Device1.Dispose();
D3D11DeviceContext1.Dispose();
SwapChain.Dispose();
}
}
以上代码使用的 StepPerformanceCounter 只是一个调试辅助代码,用于记录耗时,具体实现在此略过
项目文件 csproj 代码如下
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct2D1" Version="3.8.2" />
<PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
<PackageReference Include="Vortice.DirectComposition" Version="3.8.2" />
<PackageReference Include="Vortice.DXGI" Version="3.8.2" />
<PackageReference Include="Vortice.Win32" Version="2.3.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.257">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="MicroCom.Runtime" Version="0.11.0" />
</ItemGroup>
</Project>
可见是支持 AOT 发布的,可在此基础上扩展出一个高性能低延迟笔迹应用的画板部分。但需要说明的是,即使上了此技术,也只是追平 WPF 的笔迹应用性能而已。如对触摸相关感兴趣,还请参阅 WPF 触摸相关
依赖的 CsWin32 配置的 NativeMethods.txt 文件的代码如下
EnumDisplayMonitors
GetMonitorInfo
MONITORINFOEXW
EnumDisplaySettings
GetDisplayConfigBufferSizes
QueryDisplayConfig
DisplayConfigGetDeviceInfo
RegisterClassEx
GetModuleHandle
LoadCursor
IDC_ARROW
CreateWindowEx
CW_USEDEFAULT
ShowWindow
SHOW_WINDOW_CMD
GetMessage
TranslateMessage
DispatchMessage
DefWindowProc
GetClientRect
GetWindowLong
SetWindowLong
NCCALCSIZE_PARAMS
WaitForSingleObjectEx
ClientToScreen
WM_POINTERUPDATE
GetPointerDeviceRects
GetPointerTouchInfo
EnableMouseInPointer
如不知道整个项目是如何组织的,还请按照如下方法拉取所有的代码获取全部代码
本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2581b6d3b962e1f9912ebf359de3afbda4ab7e78
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 2581b6d3b962e1f9912ebf359de3afbda4ab7e78
获取代码之后,进入 DirectX/D2D/KearjerijarqaloChurharcarwaya 文件夹,即可获取到源代码。欢迎大家拉下来代码跑跑看性能,这个简单的应用能够追平 WPF 的笔迹应用的性能,可以看到矩形左上角的点能够完全追平系统触摸反馈点的中心点。如果在触摸移动过程中,移动鼠标,让鼠标光标显示,则可以看到矩形左上角稍微落后鼠标光标一点点。如此即可证明此方案能够获得比较低的输入延迟
更多渲染和触摸博客,请参阅 博客导航
参考文档:
Reduce latency with DXGI 1.3 swap chains - UWP applications - Microsoft Learn