在 DirectComposition 里面提供了 Commit 机制,一次 Commit 的所有内容都能在相同的一帧在屏幕显示出来,如此可以非常方便地完成渲染对齐任务
通过 WaitForCommitCompletion 方法可以等待 Commit 内容完成渲染,此方法作用相当于等待交换链写法的等待垂直同步实现
在 上一篇博客 中,采用了传统的 DXGI 交换链与 DirectComposition 对接
在本文这里将去掉交换链,可以很大简化对接渲染的逻辑。采用 DXGI 交换链对接的方式,可以比较方便对接原有的程序,且可以实现更高帧率的控制。而采用 DirectComposition 的 Commit 写法,可以更好利用 DirectComposition 机制,实现多表面合成以及更加实时的合成器动画
本文将给出最简实现对接的代码逻辑,其步骤如下
- 创建 Win32 窗口
- 创建 DirectComposition 设备和关联窗口,获取渲染表面
- 执行渲染逻辑
为了保持本文简洁,我将不在正文部分贴出非关键部分的代码,在本文末尾给出全部核心代码。本文的全部核心代码部分不到 200 行,适合一口气完成。本文也使用了到了一些库,为了防止大家不知道项目如何配置的,在本文末尾也给出整个项目全部代码和配置的下载方法
基础库
按照 .NET 的惯例,开始之前先安装基础库,安装之后的 csproj 项目文件代码大概如下- Exe net10.0 enable true true all runtime; build; native; contentfiles; analyzers
复制代码 创建 Win32 窗口
创建 Win32 窗口仅仅只是想拿到窗口句柄,不是本文重点,这里就忽略 CreateWindow 方法的实现- // 创建窗口 HWND window = CreateWindow(); // 显示窗口 ShowWindow(window, SHOW_WINDOW_CMD.SW_NORMAL);
复制代码 以上代码的 ShowWindow 是标准的 Win32 方法,由 CsWin32 库生成。定义如下- [DllImport("USER32.dll", ExactSpelling = true),DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [SupportedOSPlatform("windows5.0")] internal static extern winmdroot.Foundation.BOOL ShowWindow(winmdroot.Foundation.HWND hWnd, winmdroot.UI.WindowsAndMessaging.SHOW_WINDOW_CMD nCmdShow);
复制代码 为了直接使用方法,在本文这里直接在命名空间引用静态类,代码如下- using static Windows.Win32.PInvoke;
复制代码 创建 DirectComposition 设备
先使用以下快速地代码创建 ID3D11Device 设备。正常来说,还是会尝试遍历获取显示适配器用来手动创建设备。本文这里使用的是比较不稳妥的简化写法。正确且常用的写法代码稍多,在本文这里 ID3D11Device 不是主角。如对此感兴趣,请参阅 渲染博客导航- var result = D3D11.D3D11CreateDevice(null, DriverType.Hardware, DeviceCreationFlags.BgraSupport, featureLevels: [], out ID3D11Device iD3D11Device, out var feature, out ID3D11DeviceContext iD3D11DeviceContext); result.CheckError(); _ = feature; iD3D11DeviceContext.Dispose(); // 用不着就先释放。释放不代表立刻回收资源,只是表示业务层不需要用到,减少引用计数
复制代码 在本文这里只用到了 ID3D11Device 设备,于是就选择立刻释放 ID3D11DeviceContext 的引用
拿到 ID3D11Device 设备,就可以调用 DComp.DCompositionCreateDevice3 创建 IDCompositionDevice 设备,代码如下- IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3(iD3D11Device);
复制代码 这里的 IDCompositionDevice 应该就是 IDirectCompositionDevice 的缩写
对接窗口
通过 CreateTargetForHwnd 方法可以将 DirectComposition 关联到窗口上,代码如下- IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3(iD3D11Device); compositionDevice.CreateTargetForHwnd(window, topmost: true, out IDCompositionTarget compositionTarget);
复制代码 这里的 CreateTargetForHwnd 的第二个参数比较迷惑,以上的 CreateTargetForHwnd 参数 topmost 不是值窗口置顶,而是:如果视觉树应显示在由 hwnd 参数指定的窗口子元素之上,则为 TRUE;否则,视觉树将显示在子元素之后。原文:
TRUE if the visual tree should be displayed on top of the children of the window specified by the hwnd parameter; otherwise, the visual tree is displayed behind the children.
详细请看 https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd
创建视觉对象
创建视觉对象之前,需要获取当前窗口的尺寸,代码如下- RECT windowRect; PInvoke.GetClientRect(window, &windowRect); var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);
复制代码 创建视觉对象只需简单地调用 IDCompositionDevice.CreateVisual 方法,然而此时视觉对象还没有内容,可通过 IDCompositionDevice.CreateVirtualSurface 创建表面来作为内容,简单写法如下- IDCompositionVisual compositionVisual = compositionDevice.CreateVisual(); IDCompositionVirtualSurface surface = compositionDevice.CreateVirtualSurface((uint) clientSize.Width, (uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied); compositionVisual.SetContent(surface);
复制代码 创建表面时,可用 CreateVirtualSurface 或 CreateSurface 方法。两者不同的是 CreateVirtualSurface 创建的是稀疏表面,而 CreateSurface 是大数组(矩阵),在调用 BeginDraw 之前 IDCompositionVirtualSurface 不会分别空间,且 IDCompositionVirtualSurface 还能 Resize 重新设置大小。而 CreateSurface 则就不能。使用 CreateSurface 的例子如下- var createSurfaceResult = compositionDevice.CreateSurface((uint) clientSize.Width, (uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied, out IDCompositionSurface? dCompositionSurface);createSurfaceResult.CheckError();
复制代码 创建表面需要传染颜色格式和对 AlphaMode 的处理,本文这里传入的是 Premultiplied 采用预乘方法。对 Premultiplied 简单来说就是最终输出的值里的 RGB 分量都乘以透明度。更多细节请参阅 支持的像素格式和 Alpha 模式 - Win32 apps - Microsoft Learn
传入 Premultiplied 预乘时,会更多占用 DWM 的资源,在 4K 的全屏窗口上,对比 Premultiplied 预乘与忽略 AlphaMode 的性能,可以看到预乘会比忽略占用多非常多的 DWM 资源。如果自己的应用是无需窗口背景透明的,还请设置为忽略 AlphaMode 模式
配置视觉对象
完成视觉对象创建之后,可将此视觉对象设置为窗口的根内容。在 DirectComposition 里可以设置视觉树,一个视觉对象上可以添加很多个视觉对象,但只有其中一个可以成为 IDCompositionTarget 的 Root 视觉对象- compositionTarget.SetRoot(compositionVisual); compositionDevice.Commit(); // 非必须
复制代码 对接 D2D 渲染
以上代码就完成了视觉对象的创建和在窗口上显示的基础逻辑。为了能够绘制漂亮的界面,在本文这里将和 D2D 进行对接
为了能够和 D2D 进行对接,需要给 D2D 一个绘制表面。从 IDCompositionVirtualSurface 或 IDCompositionSurface 表面调用 BeginDraw 方法,即可获取到 IDXGISurface 表面,从而让 D2D 在此表面上绘制,基础逻辑如下- IDXGISurface dxgiSurface = surface.BeginDraw(null, out var updateOffset);
复制代码 再按照 D2D 的初始化方法,将 D2D 的 ID2D1RenderTarget 创建出来,代码如下- // 工厂创建只需一次 Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory(); // 以下循环为每一帧执行 while (!_isMainWindowClosed) { using IDXGISurface dxgiSurface = surface.BeginDraw(null, out var updateOffset); var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties() { PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied), Type = Vortice.Direct2D1.RenderTargetType.Hardware, }; using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget = d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget; ... // 忽略其他代码 }
复制代码 拿到 ID2D1RenderTarget 即可进行 D2D 的绘制,在本文这里使用了名为 D2DRenderDemo 的辅助类进行绘制,具体代码可以在后文找到- // 以下循环为每一帧执行 while (!_isMainWindowClosed) { using IDXGISurface dxgiSurface = surface.BeginDraw(null, out var updateOffset); var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties() { PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied), Type = Vortice.Direct2D1.RenderTargetType.Hardware, }; using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget = d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget; renderTarget.BeginDraw(); // 在这里编写绘制逻辑 renderTarget.Clear(new Color4(0f)); d2DRenderDemo.Draw(renderTarget, clientSize); renderTarget.EndDraw(); ... // 忽略其他代码 }
复制代码 当 D2D 绘制完成之后,需要调用 IDCompositionVirtualSurface 或 IDCompositionSurface 的 EndDraw 方法。再调用 IDCompositionDevice 的 Commit 方法将内容提交出去。当所有的窗口都完成绘制和 Commit 之后,调用 IDCompositionDevice.WaitForCommitCompletion 等待 DWM 消费。调用 IDCompositionDevice.WaitForCommitCompletion 约等于等待垂直同步,等待界面刷新- renderTarget.BeginDraw(); // 在这里编写绘制逻辑 renderTarget.Clear(new Color4(0f)); d2DRenderDemo.Draw(renderTarget, clientSize); renderTarget.EndDraw(); surface.EndDraw(); compositionDevice.Commit(); compositionDevice.WaitForCommitCompletion();
复制代码 在本文这里,由于只有一个窗口,于是在 Commit 之后即可立刻调用 WaitForCommitCompletion 方法了
整个渲染代码,即每一帧跑的代码如下- Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory(); var d2DRenderDemo = new D2DRenderDemo(); while (!_isMainWindowClosed) { using IDXGISurface dxgiSurface = surface.BeginDraw(null, out var updateOffset); _ = updateOffset; var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties() { PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied), Type = Vortice.Direct2D1.RenderTargetType.Hardware, }; using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget = d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget; renderTarget.BeginDraw(); // 在这里编写绘制逻辑 renderTarget.Clear(new Color4(0f)); d2DRenderDemo.Draw(renderTarget, clientSize); renderTarget.EndDraw(); surface.EndDraw(); compositionDevice.Commit(); compositionDevice.WaitForCommitCompletion(); while (true) { var success = PInvoke.PeekMessage(out var message, window, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE); if (!success) { break; } PInvoke.TranslateMessage(&message); PInvoke.DispatchMessage(&message); } }
复制代码 以上代码也跑了 PeekMessage 方法防止窗口未响应
在每次进入绘制的时候调用 renderTarget.Clear(new Color4(0f)); 可以解决双缓存带来的闪烁问题,即界面内容被分别不同步地绘制到两个纹理表面上。通过 Clear 确保每次都是重新绘制,解决此问题
什么时候可以不做清理而进行绘制?进行某些性能优化的时候,且此时应该确保绘制同步。或再开一个表面纹理,通过同步的方式再将最终界面绘制
核心代码
核心的代码被我放在一个文件里面,代码如下- using System;using System.Collections.Generic;using System.Runtime.InteropServices;using System.Runtime.Versioning;using Vortice.DCommon;using Vortice.Direct3D;using Vortice.Direct3D11;using Vortice.DirectComposition;using Vortice.DXGI;using Vortice.Mathematics;using Windows.Win32;using Windows.Win32.Foundation;using Windows.Win32.Graphics.Gdi;using Windows.Win32.UI.WindowsAndMessaging;using AlphaMode = Vortice.DXGI.AlphaMode;using D2D = Vortice.Direct2D1;namespace DijallnemrecerkuCheberewhibair;[SupportedOSPlatform("windows6.1")]class DirectCompositionDemo{ public unsafe void Run() { var window = CreateWindow(); PInvoke.ShowWindow(window, SHOW_WINDOW_CMD.SW_MAXIMIZE); var result = D3D11.D3D11CreateDevice(null, DriverType.Hardware, DeviceCreationFlags.BgraSupport, featureLevels: [], out ID3D11Device iD3D11Device, out var feature, out ID3D11DeviceContext iD3D11DeviceContext); result.CheckError(); _ = feature; iD3D11DeviceContext.Dispose(); // 用不着就先释放。释放不代表立刻回收资源,只是表示业务层不需要用到,减少引用计数 IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3(iD3D11Device); compositionDevice.CreateTargetForHwnd(window, topmost: true, out IDCompositionTarget compositionTarget); // 以上的 CreateTargetForHwnd 参数 topmost 不是值窗口置顶,而是:如果视觉树应显示在由 hwnd 参数指定的窗口子元素之上,则为 TRUE;否则,视觉树将显示在子元素之后 // > TRUE if the visual tree should be displayed on top of the children of the window specified by the hwnd parameter; otherwise, the visual tree is displayed behind the children. // https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd // 创建视觉对象 RECT windowRect; PInvoke.GetClientRect(window, &windowRect); var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top); IDCompositionVisual compositionVisual = compositionDevice.CreateVisual(); IDCompositionVirtualSurface surface = compositionDevice.CreateVirtualSurface((uint) clientSize.Width, (uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied); // 创建 IDCompositionSurface 有两个方法,分别是 CreateVirtualSurface 和 CreateSurface 方法。两者不同的是 CreateVirtualSurface 创建的是稀疏表面,而 CreateSurface 是大数组(矩阵),在调用 BeginDraw 之前 IDCompositionVirtualSurface 不会分别空间,且 IDCompositionVirtualSurface 还能 Resize 重新设置大小。而 CreateSurface 则就不能 //var createSurfaceResult = compositionDevice.CreateSurface((uint) clientSize.Width, // (uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied, out IDCompositionSurface? dCompositionSurface); //createSurfaceResult.CheckError(); //surface = dCompositionSurface; compositionVisual.SetContent(surface); compositionTarget.SetRoot(compositionVisual); compositionDevice.Commit(); Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory(); var d2DRenderDemo = new D2DRenderDemo(); while (!_isMainWindowClosed) { using IDXGISurface dxgiSurface = surface.BeginDraw(null, out var updateOffset); _ = updateOffset; var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties() { PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied), Type = Vortice.Direct2D1.RenderTargetType.Hardware, }; using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget = d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties); Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget; renderTarget.BeginDraw(); // 在这里编写绘制逻辑 renderTarget.Clear(new Color4(0f)); d2DRenderDemo.Draw(renderTarget, clientSize); renderTarget.EndDraw(); surface.EndDraw(); compositionDevice.Commit(); compositionDevice.WaitForCommitCompletion(); while (true) { var success = PInvoke.PeekMessage(out var message, window, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE); if (!success) { break; } PInvoke.TranslateMessage(&message); PInvoke.DispatchMessage(&message); } } } private bool _isMainWindowClosed; private unsafe HWND CreateWindow() { PInvoke.DwmIsCompositionEnabled(out var compositionEnabled); if (!compositionEnabled) { Console.WriteLine($"无法启用透明窗口效果"); } // [Windows 窗口样式 什么是 WS_EX_NOREDIRECTIONBITMAP 样式](https://blog.lindexi.com/post/Windows-%E7%AA%97%E5%8F%A3%E6%A0%B7%E5%BC%8F-%E4%BB%80%E4%B9%88%E6%98%AF-WS_EX_NOREDIRECTIONBITMAP-%E6%A0%B7%E5%BC%8F.html ) WINDOW_EX_STYLE exStyle = WINDOW_EX_STYLE.WS_EX_NOREDIRECTIONBITMAP; var style = WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW; var defaultCursor = PInvoke.LoadCursor( new HINSTANCE(IntPtr.Zero), new PCWSTR(PInvoke.IDC_ARROW.Value)); var className = $"lindexi-{Guid.NewGuid().ToString()}"; var title = "The Title"; _wndProcDelegate = new WNDPROC(WndProc); // 仅用于防止 GC 回收。详细请看 https://github.com/lindexi/lindexi_gd/pull/85 fixed (char* pClassName = className) fixed (char* pTitle = title) { var wndClassEx = new WNDCLASSEXW { cbSize = (uint) Marshal.SizeOf(), style = style, lpfnWndProc = _wndProcDelegate, hInstance = new HINSTANCE(PInvoke.GetModuleHandle(null).DangerousGetHandle()), hCursor = defaultCursor, hbrBackground = new HBRUSH(IntPtr.Zero), lpszClassName = new PCWSTR(pClassName) }; ushort atom = PInvoke.RegisterClassEx(in wndClassEx); var dwStyle = WINDOW_STYLE.WS_OVERLAPPEDWINDOW | WINDOW_STYLE.WS_VISIBLE; var windowHwnd = PInvoke.CreateWindowEx( exStyle, new PCWSTR((char*) atom), new PCWSTR(pTitle), dwStyle, 0, 0, 1900, 1000, HWND.Null, HMENU.Null, HINSTANCE.Null, null); return windowHwnd; } } private WNDPROC? _wndProcDelegate; private LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam) { if (message == PInvoke.WM_CLOSE) { _isMainWindowClosed = true; } return PInvoke.DefWindowProc(hwnd, message, wParam, lParam); }}class D2DRenderDemo{ // 此为调试代码,绘制一些矩形条 private List? _renderList; public void OnReSize() { _renderList = null; } public void Draw(D2D.ID2D1RenderTarget renderTarget, SizeI clientSize) { var rectWeight = 10; var rectHeight = 20; var margin = 5; if (_renderList is null) { _renderList = new List(); for (int top = margin; top < clientSize.Height - rectHeight - margin; top += rectHeight + margin) { Rect rect = new Rect(margin, top, rectWeight, rectHeight); var color = new Color4(Random.Shared.NextSingle(), Random.Shared.NextSingle(), Random.Shared.NextSingle()); var step = Random.Shared.Next(1, 20); var renderInfo = new D2DRenderInfo(rect, step, color); _renderList.Add(renderInfo); } } for (var i = 0; i < _renderList.Count; i++) { var renderInfo = _renderList[i]; using var brush = renderTarget.CreateSolidColorBrush(renderInfo.Color); renderTarget.FillRectangle(renderInfo.Rect, brush); var nextRect = renderInfo.Rect with { Width = renderInfo.Rect.Width + renderInfo.Step }; if (nextRect.Width > clientSize.Width - margin * 2) { nextRect = nextRect with { Width = rectWeight }; } _renderList[i] = renderInfo with { Rect = nextRect }; } } private readonly record struct D2DRenderInfo(Rect Rect, int Step, Color4 Color);}
复制代码 全部代码
本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码- git initgit remote add origin https://gitee.com/lindexi/lindexi_gd.gitgit pull origin daef15201848fb5c338f519d49f4879017590124
复制代码 以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码- git remote remove origingit remote add origin https://github.com/lindexi/lindexi_gd.gitgit pull origin daef15201848fb5c338f519d49f4879017590124
复制代码 获取代码之后,进入 DirectX/DirectComposition/DijallnemrecerkuCheberewhibair 文件夹,即可获取到源代码
更多博客
渲染部分,关于 SharpDx 和 Vortice 的使用方法,包括入门级教程,请参阅:
更多关于我博客请参阅 博客导航
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |