在 上一篇博客 和大家介紹瞭如何在控制枱裏面用裸 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 消息。接收到之後,將信息進行緩存
- 當 渲染線程 獲得渲染時機時,取最後一個 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.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 對象
IDXGISwapChain2 swapChain2 = swapChain1.QueryInterface<IDXGISwapChain2>();
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.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 用於渲染,其核心代碼如下
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 的座標點,其代碼如下
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 方法傳遞到渲染線程
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.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 代碼如下
<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