僭墙覆 发表于 2025-8-5 07:25:19

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

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

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

最初,我选择使用 Direct2D 的 DrawLine 方法来逐段绘制轨迹。代码的逻辑非常直观,就是遍历轨迹点,然后两两相连画线:
for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
{
    StarSnapshot star = _lastSnapshot.Stars;
    StarUIProps prop = _uiProps;

    // 遍历每两个相邻的点,并绘制一条线段
    prop.TrackingHistory.Enumerate2((Vector2 from, Vector2 to, int i) =>
    {
      // 根据点的位置计算一个渐变透明度
      float alpha = 1.0f * i / (prop.TrackingHistory.Count - 1);
      Color4 color = new Color4(prop.Color.R, prop.Color.G, prop.Color.B, alpha);
      
      // 调用DrawLine API
      ctx.DrawLine(from, to, XResource.GetColor(color), 0.02f);
    });
}在轨迹点不多的时候,这套方案跑得非常欢快。然而,当用户希望看到更长、更华丽的轨迹时,问题就暴露了。当点的数量达到 10万 个级别时,界面开始出现肉眼可见的卡顿和掉帧。很显然,性能瓶颈出现了,优化迫在眉睫。
量化问题:用数据说话

为了精准定位问题,我进行了一次简单的性能测试。我使用 Stopwatch 来记录在轨迹点数达到10万个时,整个绘制过程的耗时。
protected override void OnDraw(ID2D1DeviceContext ctx)
{
    // ... 其他绘制准备工作 ...

    Stopwatch sw = Stopwatch.StartNew();

    DrawCore(ctx); // 核心绘制逻辑

    // 当轨迹点达到10万时,打印耗时
    if (_uiProps.TrackingHistory.Count == 100000)
    {
      sw.Elapsed.TotalMilliseconds.Dump();
    }
   
    // ... 其他效果处理 ...
}测试结果相当不乐观,连续几次的耗时输出如下:
50.0262
51.7592
51.0839
50.7521
50.838平均耗时稳定在 50毫秒 左右!这是一个什么概念?为了保证流畅的用户体验(比如 60 FPS),每一帧的渲染时间必须控制在 16.67毫秒 以内。现在 50 毫秒的耗时,意味着帧率已经掉到了 20 FPS 以下,卡顿是必然的结果。
柳暗花明:一次调用胜过十万次

既然 DrawLine 的循环调用是瓶颈,那么优化的思路就应该是减少调用的次数。在和朋友讨论后,我决定尝试使用 ID2D1PathGeometry 来重构绘制逻辑。
ID2D1PathGeometry 允许我们先在内存中构建一个完整的几何路径,然后一次性地将其提交给 GPU 进行绘制。新的代码如下:
// 先绘制轨迹
for (int i = 0; i < _lastSnapshot.Stars.Length; ++i)
{
    StarSnapshot star = _lastSnapshot.Stars;
    StarUIProps prop = _uiProps;

    if (prop.TrackingHistory.Count < 2) continue;

    // 1. 创建一个路径几何对象
    using ID2D1PathGeometry1 path = XResource.Direct2DFactory.CreatePathGeometry();
   
    // 2. 打开路径并获取一个"画笔" (GeometrySink)
    using ID2D1GeometrySink sink = path.Open();
   
    // 3. 定义路径的起点
    sink.BeginFigure(prop.TrackingHistory.First!.Value, FigureBegin.Hollow);
   
    // 4. 将所有的点批量添加到路径中
    prop.TrackingHistory.Enumerate((pt, index) =>
    {
      if (index > 0) { sink.AddLine(pt); }
    });
   
    // 5. 结束并关闭路径定义
    sink.EndFigure(FigureEnd.Open);
    sink.Close();
   
    // 6. 一次性将整个路径绘制出来
    ctx.DrawGeometry(path, XResource.GetColor(prop.Color), 0.02f);
}改完代码后,我怀着忐忑的心情再次运行性能测试,结果让我大吃一惊:
6.8739
6.4511
6.436
6.0901
5.9227
平均耗时骤降到了 6毫秒 左右!性能几乎提升了 10倍!
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页: [1]
查看完整版本: 更复杂的代码,为何跑得快了10倍?一次Draw Call优化引发的思考