本文将分享我在 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
发表评论