本文将分享我在 dotnet 里面使用 direct2d 配合 aot 开发一个简单的测试应用的经验。这是我用不到 370 行代码,从零开始控制台创建 win32 窗口,再挂上交换链,在窗口上使用 d2d 绘制界面内容,最后使用 aot 方式发布的测试应用。成品文件体积不超过 10mb 且运行内存稳定在 60mb 以内,满帧率运行但 cpu 近乎不动
此测试应用通过 win32 裸窗口创建方式创建窗口且开启窗口消息循环。使用 direct2d 进行界面绘制,可以比较方便绘制出复杂且绚丽的界面,整体使用类似于直接使用 wpf 的 drawingcontext 绘制界面内容。整体应用只依赖 d2d 绘制界面以及一点点 win32 函数用来创建窗口,除此之外没有其他的依赖。这是一个完全彻底的原生应用,且由于直接通过 d2d 绘制渲染,没有中间的框架层,整体的渲染效率不错,可以达成满帧率运行但 cpu 近乎不动的效果。以下是我的制作过程所需的依赖库和框架
整个测试应用采用了 .net 8 的框架,用于更好的支持 aot 发布
使用了 vortice 系列库用于对 directx 的封装,方便让编写调用 directx 的代码
使用了 microsoft.windows.cswin32 方便进行 win32 方法的调用
所有的代码都写在 program.cs 文件里面,代码长度不到 370 行,更有趣的是,可以强行算是都写在 main 方法里面。全部代码由 main 方法以及放在 main 方法里面的局部方法构成。整体实现非常简单。我将会在本文末尾告诉大家本文的代码的下载方法
本文仅仅是分享我的开发经验,不包含 directx 的前置知识。如果不熟悉 d2d 和 directx 还请以看着玩的心态阅读本文
一开始采用了 directx 使用 vortice 从零开始控制台创建 direct2d1 窗口修改颜色 和 dotnet directx 通过 vortice 控制台使用 id2d1devicecontext 绘制画面 博客提供的方法搭建了基础的应用框架
为了让界面更加的丰富,我准备在界面添加多个圆形。然后为了让界面动起来,我添加了名为 drawinginfo 的结构体,用于存放每个圆形的坐标和大小等信息
readonly record struct drawinginfo(system.numerics.vector2 offset, size size, d2d.id2d1solidcolorbrush brush);
先在绘制的循环外对 drawinginfo 进行随机设置值
var ellipseinfolist = new list<drawinginfo>(); for (int i = 0; i < 3000; i++) { // 随意创建颜色 var color = new color4((byte) random.shared.next(255), (byte) random.shared.next(255), (byte) random.shared.next(255)); d2d.id2d1solidcolorbrush brush = rendertarget.createsolidcolorbrush(color); ellipseinfolist.add(new drawinginfo(new system.numerics.vector2(random.shared.next(clientsize.width), random.shared.next(clientsize.height)), new size(random.shared.next(10, 100)), brush)); }
进入循环之后,再每次修改 offset 的值,这样就可以让每次绘制的圆形动起来
while (true) { // 开始绘制逻辑 rendertarget.begindraw(); // 清空画布 rendertarget.clear(new color4(0xff, 0xff, 0xff)); // 在下面绘制漂亮的界面 for (var i = 0; i < ellipseinfolist.count; i++) { var drawinginfo = ellipseinfolist[i]; var vector2 = drawinginfo.offset; vector2.x += random.shared.next(200) - 100; vector2.y += random.shared.next(200) - 100; while (vector2.x < 100 || vector2.x > clientsize.width - 100) { vector2.x = random.shared.next(clientsize.width); } while (vector2.y < 100 || vector2.y > clientsize.height - 100) { vector2.y = random.shared.next(clientsize.height); } ellipseinfolist[i] = drawinginfo with { offset = vector2 }; // 忽略其他代码 } // 忽略其他代码 }
以上的修改坐标代码只是为了让圆形每次都在其附近移动
附带就在里层循环将每个圆形绘制,代码如下
// 在下面绘制漂亮的界面 for (var i = 0; i < ellipseinfolist.count; i++) { // 忽略其他代码 rendertarget.fillellipse(new d2d.ellipse(vector2, drawinginfo.size.width, drawinginfo.size.height), drawinginfo.brush); }
大概的改动如此,接下来咱需要改造一下 csproj 项目文件,让此项目可以构建出 aot 版本的应用
先修改 targetframework 为 net8.0 使用 .net 8 可以更好构建 aot 应用
<propertygroup> <targetframework>net8.0</targetframework> </propertygroup>
接着为了减少不断提示的平台警告,添加以下代码忽略 ca1416 警告
<propertygroup> <targetframework>net8.0</targetframework> <nowarn>ca1416</nowarn> </propertygroup>
接着再添加 publishaot 属性,这样调用发布命令之后,就可以自动创建 aot 应用的文件
<propertygroup> <targetframework>net8.0</targetframework> <nowarn>ca1416</nowarn> <publishaot>true</publishaot> </propertygroup>
此时运行起来将不会成功,将会提示大概如下的错误
unhandled exception: system.missingmethodexception: no parameterless constructor defined for type 'vortice.dxgi.idxgifactory2'. at system.activatorimplementation.createinstance(type, bindingflags, binder, object[], cultureinfo, object[]) + 0x348 at sharpgen.runtime.marshallinghelpers.frompointer[t](intptr) + 0x8c at vortice.dxgi.dxgi.createdxgifactory1[t]() + 0x55 at program.<<main>$>g__created2d|0_2(program.<>c__displayclass0_0&) + 0x90 at program.<main>$(string[] args) + 0x23e at cedageawhakairnerewhalnaibiferenagifee!<baseaddress>+0x17a3c0
或者是如下的错误
unhandled exception: system.missingmethodexception: no parameterless constructor defined for type 'vortice.direct3d11.id3d11device1'. at system.activatorimplementation.createinstance(type, bindingflags, binder, object[], cultureinfo, object[]) + 0x348 at sharpgen.runtime.marshallinghelpers.frompointer[t](intptr) + 0x8c at sharpgen.runtime.comobject.queryinterface[t]() + 0x64 at program.<<main>$>g__created2d|0_2(program.<>c__displayclass0_0&) + 0x1c7 at program.<main>$(string[] args) + 0x23e at cedageawhakairnerewhalnaibiferenagifee!<baseaddress>+0x335cf0
这是因为这些引用的库里面的类型在 aot 的裁剪过程被丢掉
修复的方法很简单,那就是将 vortice 添加到 trimmerrootassembly 里面,防止在 aot 过程被裁剪
<itemgroup> <trimmerrootassembly include="vortice.win32"/> <trimmerrootassembly include="vortice.dxgi"/> <trimmerrootassembly include="vortice.direct3d11"/> <trimmerrootassembly include="vortice.direct2d1"/> <trimmerrootassembly include="vortice.d3dcompiler"/> <trimmerrootassembly include="vortice.directx"/> <trimmerrootassembly include="vortice.mathematics"/> </itemgroup>
修改之后的 csproj 代码如下
<project sdk="microsoft.net.sdk"> <propertygroup> <outputtype>exe</outputtype> <targetframework>net8.0</targetframework> <implicitusings>enable</implicitusings> <nullable>enable</nullable> <publishaot>true</publishaot> <nowarn>ca1416</nowarn> </propertygroup> <itemgroup> <packagereference include="vortice.direct2d1" version="2.1.32" /> <packagereference include="vortice.direct3d11" version="2.1.32" /> <packagereference include="vortice.directx" version="2.1.32" /> <packagereference include="vortice.d3dcompiler" version="2.1.32" /> <packagereference include="vortice.win32" version="1.6.2" /> <packagereference include="microsoft.windows.cswin32" privateassets="all" version="0.2.63-beta" /> </itemgroup> <itemgroup> <trimmerrootassembly include="vortice.win32"/> <trimmerrootassembly include="vortice.dxgi"/> <trimmerrootassembly include="vortice.direct3d11"/> <trimmerrootassembly include="vortice.direct2d1"/> <trimmerrootassembly include="vortice.d3dcompiler"/> <trimmerrootassembly include="vortice.directx"/> <trimmerrootassembly include="vortice.mathematics"/> </itemgroup> </project>
完成以上配置之后,即可使用命令行 dotnet publish 将项目进行发布,如果在发布的控制台可以看到 generating native code 输出,那就证明配置正确,正在构建 aot 文件
完成构建之后,即可在 bin\release\net8.0\win-x64\publish
文件夹找到构建输出的文件,在我这里看到的输出文件大小大概在 10mb 以下,大家可以尝试使用本文末尾的方法拉取我的代码自己构建一下,试试效果
运行起来的任务管理器所见内存大小大约是 30mb 左右,通过 vmmap 工具查看 workingset 和 private bytes 都在 60mb 以内。虽然 committed 的内存高达 300mb 但是绝大部分都是 image 共享部分占用内存,如显卡驱动等部分的占用,这部分占用大约在 250mb 以上,实际的 image 的 private 的占用不到 10mb 大小
我认为这个技术可以用来制作一些小而美的工具,甚至是不用考虑 x86 的,只需考虑 x64 的机器上运行的应用的安装包制作程序。要是拿着 d2d 绘制的界面去当安装包的界面,那估计安装包行业会卷起来
以下是所有的代码
using d3d = vortice.direct3d; using d3d11 = vortice.direct3d11; using dxgi = vortice.dxgi; using d2d = vortice.direct2d1; using system.runtime.compilerservices; using system.runtime.interopservices; using windows.win32.foundation; using windows.win32.ui.windowsandmessaging; using static windows.win32.pinvoke; using static windows.win32.ui.windowsandmessaging.peek_message_remove_type; using static windows.win32.ui.windowsandmessaging.wndclass_styles; using static windows.win32.ui.windowsandmessaging.window_style; using static windows.win32.ui.windowsandmessaging.window_ex_style; using static windows.win32.ui.windowsandmessaging.system_metrics_index; using static windows.win32.ui.windowsandmessaging.show_window_cmd; using vortice.dcommon; using vortice.mathematics; using alphamode = vortice.dxgi.alphamode; using system.diagnostics; unsafe { sizei clientsize = new sizei(1000, 1000); // 窗口标题 var title = "lindexi d2d aot"; var windowclassname = title; window_style style = ws_caption | ws_sysmenu | ws_minimizebox | ws_clipsiblings | ws_border | ws_dlgframe | ws_thickframe | ws_group | ws_tabstop | ws_sizebox; var rect = new rect { right = clientsize.width, bottom = clientsize.height }; adjustwindowrectex(&rect, style, false, ws_ex_appwindow); int x = 0; int y = 0; int windowwidth = rect.right - rect.left; int windowheight = rect.bottom - rect.top; // 随便,放在屏幕中间好了。多个显示器?忽略 int screenwidth = getsystemmetrics(sm_cxscreen); int screenheight = getsystemmetrics(sm_cyscreen); x = (screenwidth - windowwidth) / 2; y = (screenheight - windowheight) / 2; var hinstance = getmodulehandle((string) null); fixed (char* lpszclassname = windowclassname) { pcwstr szcursorname = new((char*) idc_arrow); var wndclassex = new wndclassexw { cbsize = (uint) unsafe.sizeof<wndclassexw>(), style = cs_hredraw | cs_vredraw | cs_owndc, // 核心逻辑,设置消息循环 lpfnwndproc = new wndproc(wndproc), hinstance = (hinstance) hinstance.dangerousgethandle(), hcursor = loadcursor((hinstance) intptr.zero, szcursorname), hbrbackground = (windows.win32.graphics.gdi.hbrush) intptr.zero, hicon = (hicon) intptr.zero, lpszclassname = lpszclassname }; ushort atom = registerclassex(wndclassex); if (atom == 0) { throw new invalidoperationexception($"failed to register window class. error: {marshal.getlastwin32error()}"); } } // 创建窗口 var hwnd = createwindowex ( ws_ex_appwindow, windowclassname, title, style, x, y, windowwidth, windowheight, hwndparent: default, hmenu: default, hinstance: default, lpparam: null ); // 创建完成,那就显示 showwindow(hwnd, sw_normal); created2d(); // 开个消息循环等待 windows.win32.ui.windowsandmessaging.msg msg; while (true) { if (getmessage(out msg, hwnd, 0, 0) != false) { _ = translatemessage(&msg); _ = dispatchmessage(&msg); if (msg.message is wm_quit or wm_close or 0) { return; } } } void created2d() { rect windowrect; getclientrect(hwnd, &windowrect); clientsize = new sizei(windowrect.right - windowrect.left, windowrect.bottom - windowrect.top); // 开始创建工厂创建 d3d 的逻辑 var dxgifactory2 = dxgi.dxgi.createdxgifactory1<dxgi.idxgifactory2>(); var hardwareadapter = gethardwareadapter(dxgifactory2) // 这里 tolist 只是想列出所有的 idxgiadapter1 方便调试而已。在实际代码里,大部分都是获取第一个 .tolist().firstordefault(); if (hardwareadapter == null) { throw new invalidoperationexception("cannot detect d3d11 adapter"); } // 功能等级 // [c# 从零开始写 sharpdx 应用 聊聊功能等级](https://blog.lindexi.com/post/c-%e4%bb%8e%e9%9b%b6%e5%bc%80%e5%a7%8b%e5%86%99-sharpdx-%e5%ba%94%e7%94%a8-%e8%81%8a%e8%81%8a%e5%8a%9f%e8%83%bd%e7%ad%89%e7%ba%a7.html) d3d.featurelevel[] featurelevels = new[] { d3d.featurelevel.level_11_1, d3d.featurelevel.level_11_0, d3d.featurelevel.level_10_1, d3d.featurelevel.level_10_0, d3d.featurelevel.level_9_3, d3d.featurelevel.level_9_2, d3d.featurelevel.level_9_1, }; dxgi.idxgiadapter1 adapter = hardwareadapter; d3d11.devicecreationflags creationflags = d3d11.devicecreationflags.bgrasupport; var result = d3d11.d3d11.d3d11createdevice ( adapter, d3d.drivertype.unknown, creationflags, featurelevels, out d3d11.id3d11device d3d11device, out d3d.featurelevel featurelevel, out d3d11.id3d11devicecontext d3d11devicecontext ); if (result.failure) { // 如果失败了,那就不指定显卡,走 warp 的方式 // http://go.microsoft.com/fwlink/?linkid=286690 result = d3d11.d3d11.d3d11createdevice( intptr.zero, d3d.drivertype.warp, creationflags, featurelevels, out d3d11device, out featurelevel, out d3d11devicecontext); // 如果失败,就不能继续 result.checkerror(); } // 大部分情况下,用的是 id3d11device1 和 id3d11devicecontext1 类型 // 从 id3d11device 转换为 id3d11device1 类型 var d3d11device1 = d3d11device.queryinterface<d3d11.id3d11device1>(); var d3d11devicecontext1 = d3d11devicecontext.queryinterface<d3d11.id3d11devicecontext1>(); // 转换完成,可以减少对 id3d11device1 的引用计数 // 调用 dispose 不会释放掉刚才申请的 d3d 资源,只是减少引用计数 d3d11device.dispose(); d3d11devicecontext.dispose(); // 创建设备,接下来就是关联窗口和交换链 dxgi.format colorformat = dxgi.format.b8g8r8a8_unorm; const int framecount = 2; dxgi.swapchaindescription1 swapchaindescription = new() { width = clientsize.width, height = clientsize.height, format = colorformat, buffercount = framecount, bufferusage = dxgi.usage.rendertargetoutput, sampledescription = dxgi.sampledescription.default, scaling = dxgi.scaling.stretch, swapeffect = dxgi.swapeffect.flipdiscard, alphamode = alphamode.ignore, }; // 设置是否全屏 dxgi.swapchainfullscreendescription fullscreendescription = new dxgi.swapchainfullscreendescription { windowed = true }; // 给创建出来的窗口挂上交换链 dxgi.idxgiswapchain1 swapchain = dxgifactory2.createswapchainforhwnd(d3d11device1, hwnd, swapchaindescription, fullscreendescription); // 不要被按下 alt+enter 进入全屏 dxgifactory2.makewindowassociation(hwnd, dxgi.windowassociationflags.ignorealtenter); d3d11.id3d11texture2d backbuffertexture = swapchain.getbuffer<d3d11.id3d11texture2d>(0); // 获取到 dxgi 的平面,这个平面就约等于窗口渲染内容 dxgi.idxgisurface dxgisurface = backbuffertexture.queryinterface<dxgi.idxgisurface>(); // 对接 d2d 需要创建工厂 d2d.id2d1factory1 d2dfactory = d2d.d2d1.d2d1createfactory<d2d.id2d1factory1>(); // 方法1: //var rendertargetproperties = new d2d.rendertargetproperties(pixelformat.premultiplied); //// 在窗口的 dxgi 的平面上创建 d2d 的画布,如此即可让 d2d 绘制到窗口上 //d2d.id2d1rendertarget d2d1rendertarget = // d2dfactory.createdxgisurfacerendertarget(dxgisurface, rendertargetproperties); //var rendertarget = d2d1rendertarget; // 方法2: // 创建 d2d 设备,通过设置 id2d1devicecontext 的 target 输出为 dxgisurface 从而让 id2d1devicecontext 渲染内容渲染到窗口上 // 如 https://learn.microsoft.com/en-us/windows/win32/direct2d/images/devicecontextdiagram.png 图 // 获取 dxgi 设备,用来创建 d2d 设备 dxgi.idxgidevice dxgidevice = d3d11device1.queryinterface<dxgi.idxgidevice>(); d2d.id2d1device d2ddevice = d2dfactory.createdevice(dxgidevice); d2d.id2d1devicecontext d2ddevicecontext = d2ddevice.createdevicecontext(); d2d.id2d1bitmap1 d2dbitmap = d2ddevicecontext.createbitmapfromdxgisurface(dxgisurface); d2ddevicecontext.target = d2dbitmap; var rendertarget = d2ddevicecontext; // 开启后台渲染线程,无限刷新 var stopwatch = stopwatch.startnew(); var count = 0; task.factory.startnew(() => { var ellipseinfolist = new list<drawinginfo>(); for (int i = 0; i < 100; i++) { // 随意创建颜色 var color = new color4((byte) random.shared.next(255), (byte) random.shared.next(255), (byte) random.shared.next(255)); d2d.id2d1solidcolorbrush brush = rendertarget.createsolidcolorbrush(color); ellipseinfolist.add(new drawinginfo(new system.numerics.vector2(random.shared.next(clientsize.width), random.shared.next(clientsize.height)), new size(random.shared.next(10, 100)), brush)); } while (true) { // 开始绘制逻辑 rendertarget.begindraw(); // 清空画布 rendertarget.clear(new color4(0xff, 0xff, 0xff)); // 在下面绘制漂亮的界面 for (var i = 0; i < ellipseinfolist.count; i++) { var drawinginfo = ellipseinfolist[i]; var vector2 = drawinginfo.offset; vector2.x += random.shared.next(200) - 100; vector2.y += random.shared.next(200) - 100; while (vector2.x < 100 || vector2.x > clientsize.width - 100) { vector2.x = random.shared.next(clientsize.width); } while (vector2.y < 100 || vector2.y > clientsize.height - 100) { vector2.y = random.shared.next(clientsize.height); } ellipseinfolist[i] = drawinginfo with { offset = vector2 }; rendertarget.fillellipse(new d2d.ellipse(vector2, drawinginfo.size.width, drawinginfo.size.height), drawinginfo.brush); } rendertarget.enddraw(); swapchain.present(1, dxgi.presentflags.none); // 等待刷新 d3d11devicecontext1.flush(); // 统计刷新率 count++; if (stopwatch.elapsed >= timespan.fromseconds(1)) { console.writeline($"fps: {count / stopwatch.elapsed.totalseconds}"); stopwatch.restart(); count = 0; } } }, taskcreationoptions.longrunning); } } static ienumerable<dxgi.idxgiadapter1> gethardwareadapter(dxgi.idxgifactory2 factory) { dxgi.idxgifactory6? factory6 = factory.queryinterfaceornull<dxgi.idxgifactory6>(); if (factory6 != null) { // 先告诉系统,要高性能的显卡 for (int adapterindex = 0; factory6.enumadapterbygpupreference(adapterindex, dxgi.gpupreference.highperformance, out dxgi.idxgiadapter1? adapter).success; adapterindex++) { if (adapter == null) { continue; } dxgi.adapterdescription1 desc = adapter.description1; if ((desc.flags & dxgi.adapterflags.software) != dxgi.adapterflags.none) { // don't select the basic render driver adapter. adapter.dispose(); continue; } //factory6.dispose(); console.writeline($"枚举到 {adapter.description1.description} 显卡"); yield return adapter; } factory6.dispose(); } // 如果枚举不到,那系统返回啥都可以 for (int adapterindex = 0; factory.enumadapters1(adapterindex, out dxgi.idxgiadapter1? adapter).success; adapterindex++) { dxgi.adapterdescription1 desc = adapter.description1; if ((desc.flags & dxgi.adapterflags.software) != dxgi.adapterflags.none) { // don't select the basic render driver adapter. adapter.dispose(); continue; } console.writeline($"枚举到 {adapter.description1.description} 显卡"); yield return adapter; } } static lresult wndproc(hwnd hwnd, uint message, wparam wparam, lparam lparam) { return defwindowproc(hwnd, message, wparam, lparam); } readonly record struct drawinginfo(system.numerics.vector2 offset, size size, d2d.id2d1solidcolorbrush brush);
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init git remote add origin https://gitee.com/lindexi/lindexi_gd.git git pull origin 66f9fe05baba8ad30495069aebd447b160484215
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码
git remote remove origin git remote add origin https://github.com/lindexi/lindexi_gd.git git pull origin 66f9fe05baba8ad30495069aebd447b160484215
获取代码之后,进入 cedageawhakairnerewhalnaibiferenagifee 文件夹
更多关于 directx 和 d2d 相关技术请参阅我的 博客导航
交流 vortice 技术,欢迎加群: 622808968
发表评论