找回密码
 立即注册
首页 业界区 业界 更复杂的代码,为何跑得快了10倍?一次Draw Call优化引 ...

更复杂的代码,为何跑得快了10倍?一次Draw Call优化引发的思考

僭墙覆 2025-8-5 07:25:19
大家好,最近我挖了一个新的开源项目坑:N-Body 模拟,这是一个纯粹由兴趣驱动的项目,旨在通过编程模拟天体间的万有引力,并欣赏由物理规律所生成的优美图形。
在这个项目中,有一个核心环节是绘制天体的运行轨迹。轨迹本质上是一条由无数个点连接而成的曲线。为了高效存储这些点,我使用了一个 CircularBuffer,即环形缓冲区。它的内部实现相当经典:一个数组加上两个指针,分别标记数据的有效起止位置,非常适合存储这种定长的流式数据。
1.png

初遇瓶颈:当轨迹长到令人抓狂

最初,我选择使用 Direct2D 的 DrawLine 方法来逐段绘制轨迹。代码的逻辑非常直观,就是遍历轨迹点,然后两两相连画线:
  1. for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
  2. {
  3.     StarSnapshot star = _lastSnapshot.Stars[i];
  4.     StarUIProps prop = _uiProps[i];
  5.     // 遍历每两个相邻的点,并绘制一条线段
  6.     prop.TrackingHistory.Enumerate2((Vector2 from, Vector2 to, int i) =>
  7.     {
  8.         // 根据点的位置计算一个渐变透明度
  9.         float alpha = 1.0f * i / (prop.TrackingHistory.Count - 1);
  10.         Color4 color = new Color4(prop.Color.R, prop.Color.G, prop.Color.B, alpha);
  11.         
  12.         // 调用DrawLine API
  13.         ctx.DrawLine(from, to, XResource.GetColor(color), 0.02f);
  14.     });
  15. }
复制代码
在轨迹点不多的时候,这套方案跑得非常欢快。然而,当用户希望看到更长、更华丽的轨迹时,问题就暴露了。当点的数量达到 10万 个级别时,界面开始出现肉眼可见的卡顿和掉帧。很显然,性能瓶颈出现了,优化迫在眉睫。
量化问题:用数据说话

为了精准定位问题,我进行了一次简单的性能测试。我使用 Stopwatch 来记录在轨迹点数达到10万个时,整个绘制过程的耗时。
  1. protected override void OnDraw(ID2D1DeviceContext ctx)
  2. {
  3.     // ... 其他绘制准备工作 ...
  4.     Stopwatch sw = Stopwatch.StartNew();
  5.     DrawCore(ctx); // 核心绘制逻辑
  6.     // 当轨迹点达到10万时,打印耗时
  7.     if (_uiProps[0].TrackingHistory.Count == 100000)
  8.     {
  9.         sw.Elapsed.TotalMilliseconds.Dump();
  10.     }
  11.    
  12.     // ... 其他效果处理 ...
  13. }
复制代码
测试结果相当不乐观,连续几次的耗时输出如下:
  1. 50.0262
  2. 51.7592
  3. 51.0839
  4. 50.7521
  5. 50.838
复制代码
平均耗时稳定在 50毫秒 左右!这是一个什么概念?为了保证流畅的用户体验(比如 60 FPS),每一帧的渲染时间必须控制在 16.67毫秒 以内。现在 50 毫秒的耗时,意味着帧率已经掉到了 20 FPS 以下,卡顿是必然的结果。
柳暗花明:一次调用胜过十万次

既然 DrawLine 的循环调用是瓶颈,那么优化的思路就应该是减少调用的次数。在和朋友讨论后,我决定尝试使用 ID2D1PathGeometry 来重构绘制逻辑。
ID2D1PathGeometry 允许我们先在内存中构建一个完整的几何路径,然后一次性地将其提交给 GPU 进行绘制。新的代码如下:
  1. // 先绘制轨迹
  2. for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
  3. {
  4.     StarSnapshot star = _lastSnapshot.Stars[i];
  5.     StarUIProps prop = _uiProps[i];
  6.     if (prop.TrackingHistory.Count < 2) continue;
  7.     // 1. 创建一个路径几何对象
  8.     using ID2D1PathGeometry1 path = XResource.Direct2DFactory.CreatePathGeometry();
  9.    
  10.     // 2. 打开路径并获取一个"画笔" (GeometrySink)
  11.     using ID2D1GeometrySink sink = path.Open();
  12.    
  13.     // 3. 定义路径的起点
  14.     sink.BeginFigure(prop.TrackingHistory.First!.Value, FigureBegin.Hollow);
  15.    
  16.     // 4. 将所有的点批量添加到路径中
  17.     prop.TrackingHistory.Enumerate((pt, index) =>
  18.     {
  19.         if (index > 0) { sink.AddLine(pt); }
  20.     });
  21.    
  22.     // 5. 结束并关闭路径定义
  23.     sink.EndFigure(FigureEnd.Open);
  24.     sink.Close();
  25.    
  26.     // 6. 一次性将整个路径绘制出来
  27.     ctx.DrawGeometry(path, XResource.GetColor(prop.Color), 0.02f);
  28. }
复制代码
改完代码后,我怀着忐忑的心情再次运行性能测试,结果让我大吃一惊:
  1. 6.8739
  2. 6.4511
  3. 6.436
  4. 6.0901
  5. 5.9227
复制代码
平均耗时骤降到了 6毫秒 左右!性能几乎提升了 10倍
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册