
简介一份C#调用DirectX绘制直线的性能测试示例面向入门到进阶的图形编程学习者也适合需要在WinForms/WPF中嵌入DirectX渲染并排查性能瓶颈的开发者。资源包共20个文件压缩后仅38KB内容以C#源代码为主5个.cs同时包含Visual Studio解决方案与项目文件.sln、.csproj、编译生成的exe/pdb/cache以及资源文件、配置文件与程序清单其中.cs是核心实现exe可直接运行观察效果结构清晰方便打开工程编译或单步调试。已有140人浏览学习。示例围绕“画一条直线”这一最小场景展开完整演示了DirectX设备初始化、渲染目标设置、顶点/线段绘制、Stopwatch计时等核心步骤代码中同时保留界面与逻辑实现便于对照学习C#侧DX控件的容器化管理借助断点可逐阶段分析CPU耗时定位绘制性能损耗。从这份代码中可以提取直线绘制性能测试的基本骨架理解渲染循环与计时工具的结合方式以及CS项目如何引用和封装DirectX接口适合作为图形编程课程设计或性能优化实验的起点也能作为后续绘制复杂图形的对照模板。1. 拿 C# 直接调 DX 画一条直线这个例程到底在测什么做过 WinForm 控件或自绘图表的人应该都有过这种经历GDI 画线在数据量上来之后帧率掉得没法看。想换 DirectX又怕 C# 调用 DX 的封装太复杂一个最小 Demo 都搭不起来。这个CSharp-test-DX-draw-a-line-speed项目就是干这件事的——它用最小代码量跑通 C# 与 DirectX 的交互并精确测出画一条直线到底要多少毫秒。它不是完整游戏引擎也不是控件库而是一个拿来做性能基准的起点工程。适合两类人一是想在 C# 里用 DX 做图形渲染但不知道怎么起手的人二是已经在用 GDI 画图、想量化对比 DX 性能到底强多少的人。整个工程以 Visual Studio 解决方案打包拿到手就能编译运行跑起来你在窗体上看到一条直线控制台或界面上输出绘制耗时。下面我直接拆文件、拆代码、拆计时逻辑。2. 拆解工程文件与 DirectX 的 C# 封装选型2.1 RAR 包里的文件清单与各自职责这个压缩包里的文件结构非常典型是一个标准的 C# WinForms 项目。Line.sln是解决方案入口Line.csproj是 C# 项目文件Form1.cs是主窗体逻辑Form1.Designer.cs是窗体设计器生成的界面布局代码。还有Properties目录存放程序集信息bin和obj是编译输出和中间文件。CSharp-test-DX-draw-a-line-speed/ ├── Line.sln # 解决方案文件双击用 VS 打开 ├── Line.suo # VS 用户选项缓存可删 ├── Line.csproj # 项目文件引用和编译项都在这 ├── Form1.cs # 主逻辑初始化 DX、画线、计时 ├── Form1.Designer.cs # 界面设计器代码窗体大小等 ├── Program.cs # 入口 Main 函数如存在 ├── Properties/ # AssemblyInfo 等 └── bin/ obj/ # 编译输出与中间目录这个结构说明作者是用 Visual Studio 直接创建的 WinForms 工程没有引入额外的项目依赖。Line.suo是本地用户配置文件删掉不影响编译。如果你打开解决方案发现打不开大概率是 VS 版本差异导致 .suo 文件不兼容直接删掉重新生成即可。2.2 为什么用 SlimDX 或 SharpDX 而不是原生 DirectXC# 不能直接调用 COM 风格的 DirectX 接口必须借助托管封装。历史上有两个主流选择SlimDX 和 SharpDX。SlimDX 封装更完整但停止维护较早对 .NET Framework 4.x 支持良好SharpDX 是后来的主流支持 .NET Framework 和 .NET Core文档更全性能开销也更低。这个项目里我推测用的是其中一种但两种封装的使用模式是共通的创建设备 → 创建交换链 → 获取渲染目标视图 → 绘制 → 呈现。// 以 SharpDX 为例初始化 DirectX 11 设备 var swapChainDesc new SwapChainDescription { BufferCount 1, // 窗口模式把后台缓冲区绑定到窗体句柄 IsWindowed true, OutputHandle this.Handle, // 当前窗体的句柄 SampleDescription new SampleDescription(1, 0), // 无多重采样 Width this.ClientSize.Width, Height this.ClientSize.Height, // 32 位 ARGB 颜色格式 Format Format.B8G8R8A8_UNorm, SwapEffect SwapEffect.Discard }; Device.CreateWithSwapChain( DriverType.Hardware, DeviceCreationFlags.None, swapChainDesc, out device, out swapChain);这段代码是 DX 初始化的核心骨架。OutputHandle必须指向窗体的句柄否则渲染画面无法呈现到界面上SampleDescription(1, 0)表示每个像素采样一次即关闭多重采样抗锯齿这对性能测试很重要因为抗锯齿会显著影响绘制速度。2.3 一个容易被忽略的选型细节Target Framework 版本打开Line.csproj时你会看到 TargetFrameworkVersion 节点项目可能被设置为v4.0或v4.5。SharpDX 的 NuGet 包对不同框架版本有不同的 DLL如果项目是 4.0 而引用的是 4.5 编译的 SharpDX运行时直接报找不到程序集。我一般会把这个项目的最低框架设为 .NET Framework 4.5因为 SlimDX 和 SharpDX 在这个版本下兼容性最稳不需要额外写 app.config 重定向。改完框架版本后记得重新清理并编译bin 目录里残留的旧版本 DLL 会导致奇怪的运行时错误。3. 画直线的核心流程与性能计时原理3.1 从顶点缓冲到 DrawUserPrimitives绘制一条直线在 DirectX 里不是一句DrawLine就完事的你至少需要定义两个顶点的位置和颜色然后告诉 GPU 用PrimitiveTopology.LineList这个拓扑结构去解释顶点数据。我把常见做法写出来// 定义两个顶点起点和终点 var vertices new[] { new Vector4(100.0f, 100.0f, 0.0f, 1.0f), // x, y, z, w new Vector4(500.0f, 300.0f, 0.0f, 1.0f) }; // 在 ImmediateContext 上直接传入 CPU 数据 device.ImmediateContext.DrawUserPrimitives( PrimitiveTopology.LineList, // 把每两个顶点解释为一条直线 1, // 共 1 条线段 vertices); // 顶点数据源这里有个关键点DrawUserPrimitives直接拿 CPU 内存中的顶点数组上传给 GPU不需要预先创建顶点缓冲。它适合画少量直线或一次性几何体省掉了创建缓冲区的代码。如果你要画几千条线每次调用DrawUserPrimitives都有一次 CPU → GPU 的数据传输开销这种场景应该改用VertexBuffer预先上传。3.2 Stopwatch 测时的粒度远高于毫秒性能测试用的是Stopwatch类也就是System.Diagnostics.Stopwatch。它底层调用的是高精度性能计数器QueryPerformanceCounter在 Windows 上的分辨率远高于毫秒级。代码里它的使用方式大概是这样的var stopwatch Stopwatch.StartNew(); // 开始计时 // 这里执行一次或多次画线操作 device.ImmediateContext.DrawUserPrimitives( PrimitiveTopology.LineList, 1, vertices); stopwatch.Stop(); // Elapsed 返回 TimeSpanTicks 更方便做比例计算 Console.WriteLine($画直线耗时: {stopwatch.Elapsed.TotalMilliseconds:F4} ms);TotalMilliseconds配合F4格式化输出四位小数一眼能看出微秒级别的波动。需要注意 Stopwatch 在 Windows 7 以下的系统有最小分辨率 15.6ms 的问题但 Win8 和 Win10/11 上它是高精度计时直接采信没有问题。3.3 用 IsRunning 还是计数循环来压测你打开这个项目测试时会发现单次画直线耗时极小可能只有零点几毫秒被其他系统开销掩盖。要测出有意义的数字常见做法是把画线动作循环 1000 次或 10000 次最后除以次数求平均int testCount 10000; stopwatch.Restart(); for (int i 0; i testCount; i) { device.ImmediateContext.DrawUserPrimitives( PrimitiveTopology.LineList, 1, vertices); } stopwatch.Stop(); double averageMs stopwatch.Elapsed.TotalMilliseconds / testCount; Console.WriteLine(${testCount} 次平均耗时: {averageMs:F6} ms/次);Restart方法帮你在不重新创建 Stopwatch 的情况下重置并开始计时。循环体里的操作要尽可能精简不要在循环里做顶点数据拼接或格式转换那些 CPU 开销会和 GPU 绘制混在一起测出来的数字就不是纯粹的画线耗时。3.4 绘制前的准备状态会直接影响帧率在实际全流程渲染里还要考虑清屏和缓冲区交换清屏本身用 OMSetRenderTargets 绑定渲染目标视图ClearRenderTargetView 填充背景色最后 Present 把后台缓冲区翻转到前台。这个项目的简化逻辑只关注画线这一步所以没有把清屏计入测时范围这样测的是纯粹 DCDraw Call时间——一个问题在于 Present 本身有垂直同步等待如果它是计时的终点单帧时间除了画线还包含 Vsync 等待。我做这种测试的习惯是PushDebugGroup 标记区间、OutputCapture 抓 GPU 时间戳做法太复杂一般就是靠多帧平均消掉噪声。4. 画直线性能测试的五个常见坑现象、原因与解法4.1 现象加了断点后耗时暴涨到几百毫秒现象在DrawUserPrimitives调用那一行打断点每次执行到都会中断单次画线耗时从 0.1ms 涨到两三百毫秒。原因断点触发时整个进程被挂起GPU 管线失去新的指令输入CPU 计时包含了你停在断点处的全部人工时间。解决性能测试代码里绝不能打断点。你可以用条件编译把测时逻辑包起来#if !DEBUG stopwatch.Stop(); Console.WriteLine(...); #endif4.2 现象Debug 模式比 Release 慢 5 倍以上现象同一台机器同一套代码Debug 配置下画线耗时 0.5msRelease 只有 0.08ms。原因Debug 编译没有开启代码优化C# 里数组边界检查、函数调用栈更保守JIT 生成的机器码效率低。DX API 调用本身没差这么多是封装层的间接调用被放大了。解决性能测试必须在 Release 模式下进行且要确认项目配置是 AnyCPU 的 Release 且允许不安全代码。这个项目本身很简单你可以直接改解决方案配置为 Release 再跑。4.3 现象帧率被锁死在 60FPS怎么优化都上不去现象画 1000 条和画 100 条直线的耗时几乎一样FPS 稳定在 60 左右。原因交换链的 Present 调用默认开启垂直同步也就是 “VSync”。你的绘制速度再快Present 也要等显示器的刷新信号。解决如果你只测画线本身在创建交换链时把SwapEffect设为Discard并在Present调用传 0 表示不等垂直同步swapChain.Present(0, PresentFlags.None);第一个参数是同步间隔0 表示关掉 VSync1 表示每帧同步。测性能必须传 0。4.4 现象窗口最小化后耗时变异常现象窗口最小化再还原后画线耗时偶尔跳到几毫秒然后恢复。原因最小化时 DX 渲染目标丢失或交换链缓冲被重置GPU 需要重新分配资源。解决这是正常的驱动行为。测试时固定窗口大小、全程不要最小化。你可以在程式中禁止最小化按钮或者用固定窗口尺寸的无边框窗体。4.5 现象同一段代码换台机器数字完全不同现象在 A 机器上画线耗时 0.05ms换到 B 机器成了 0.3ms。原因GPU 型号、驱动版本、显卡省电策略、CPU 频率都影响结果集成显卡和独立显卡的差异尤其巨大。解决性能测试的结果只在同一台机器上做相对比较才有意义。如果你想跨机器对比需要记录 GPU 型号和驱动版本并且保证测试期间用独显运行、关闭省电模式。这就是为什么这个项目适合做 “在你的机器上的基准”而不是和网上的数字盲目对比——我见过有人拿集显数据和别人独显数据比画线速度毫无意义。5. 进阶验证把单次画线测时扩展到帧率与批量绘制基准单次直线计时只能证明 DX 调用本身快但你要评估一个控件或渲染引擎的可用性还需要看两个延伸指标综合帧率FPS和 DPDraw Primitive数量对耗时的影响。这个项目是基准起点你自己就能扩展。常见做法是加一个渲染循环用Application.Idle事件驱动连续渲染每帧统计耗时并换算成 FPSint frameCount 0; double elapsedTime 0.0; private void OnApplicationIdle(object sender, EventArgs e) { while (Application.MessageLoop) { var stopwatch Stopwatch.StartNew(); // 清屏、画线、翻转交换链 device.ImmediateContext.ClearRenderTargetView(renderTarget, Color.CornflowerBlue); device.ImmediateContext.DrawUserPrimitives(PrimitiveTopology.LineList, 1, vertices); swapChain.Present(0, PresentFlags.None); stopwatch.Stop(); frameCount; elapsedTime stopwatch.Elapsed.TotalMilliseconds; if (elapsedTime 1000.0) // 每秒统计一次 { double fps frameCount * 1000.0 / elapsedTime; Text $FPS: {fps:F2}, 平均帧耗时: {elapsedTime / frameCount:F2} ms; frameCount 0; elapsedTime 0.0; } } }这个循环模式是 WinForms 下做 DX 渲染的标准做法。Application.Idle事件在消息队列清空之后触发能保证渲染不阻塞 UI 操作。每 1000ms 统计一次 FPS比单次 Stopwatch 测时更稳。你如果把DrawUserPrimitives的第二个参数从 1 改成 1000并传一个包含 2000 个顶点的数组就能测出批量绘制对耗时的影响趋势。我自己的血泪经验是测试画线速度时永远先关 VSync、锁 Release 模式、固定窗口尺寸三者缺一数据都是玄学。这套前提我在拿到这个项目的第一时间就做了检查从那以后每次做 WinForm 自定义控件的性能对比都强制走一遍 DX 初始化和测时检查流程先验证环境再下结论。希望帮到你。本文还有配套的精品资源点击获取