Skip to main content

Qt Canvas Painter:路径加速性能

Comments

如果您尚未了解全新的 Qt Canvas Painter,欢迎参阅此前的博客文章:简介新功能性能测试。本文将介绍路径(path)和路径组(path group),以进一步提升性能。

硬件加速渲染领域有一个普遍认知:命令式绘制速度慢,声明式绘制速度快。其背后的基本逻辑是,GPU 擅长持有数据并进行并行处理,而从 CPU 向 GPU 传输数据可能成为并行处理的瓶颈。声明式 UI 方法采用场景图(scene graph)方式,可追踪 UI 中实际需要更新的部分并最小化更新量,从而提升性能。

这一逻辑站得住脚,但我认为命令式绘制并非必然缓慢。毕竟,Vulkan、Metal 等底层图形 API 既是命令式的,又拥有极高性能。我倾向于用直接绘制来描述直接在屏幕上绘制的方式,而命令式绘制也可以面向屏幕以外的其他缓冲区。Qt Canvas Painter 的直接绘制已经过深度优化,性能极为出色,同时我们还提供了 QCanvasPath 类用于管理路径。

先从一个简单示例入手——绘制如下自定义折线图:


qcanvaspainter_graph_example_1

使用 QCanvasPainter 直接 绘制的代码如下:

p->beginPath();
p->moveTo(data[0]);
for (int i = 1; i < 40; i++)
    p->lineTo(data[i]);
p->setLineWidth(10);
p->setStrokeStyle(lineGradient);
p->stroke();

QCanvasPainter 性能极佳,渲染上述含 40 线段的折线图轻而易举。但如果路径更为复杂,且不希望在每一帧都重新生成路径,该怎么办?这正是 QCanvasPath 的用武之地。使用 QCanvasPath 实现完全相同的输出,代码如下:

if (m_path.isEmpty()) {
    m_path.moveTo(data[0]);
    for (int i = 1; i < 40; i++)
        m_path.lineTo(data[i]);
}
p->setLineWidth(10);
p->setStrokeStyle(lineGradient);
p->stroke(m_path, 1);

观察代码,可以发现几个有趣之处:

  • 绘制代码高度相似。这是有意为之——QCanvasPathQCanvasPainter 共享完全相同的绘制 API,使直接绘制与路径绘制之间的代码迁移变得轻松。

  • 路径仅在第一帧为空,后续帧将完全跳过 for 循环,直接绘制已有路径。

  • stroke 方法中有一个神秘的数字参数,这是用于缓存顶点缓冲区(vertex buffer)的可选路径组参数,可进一步提升性能。

路径方式的另一大优势在于,只需添加如下几行代码..

...
p->setLineWidth(20); p->setStrokeStyle(Qt::black); p->setAntialias(10); p->translate(-2, 4); p->stroke(m_path, 1);
...

……即可复用同一路径,以极低的额外开销添加平滑阴影效果:


qcanvaspainter_graph_example_2

QCanvasPath vs. QPainterPath

有人或许会问:Canvas Painter 为什么不直接使用现有的 QPainterPath 类?这是一个合理的问题,值得认真解答。首先,Canvas Painter 确实大量使用了 QtGui 的工具类(QColor、QFont、QImage、QTransform、QRectF 等),因此原因并非标新立异。以下是自建路径类的三大核心原因:

  1. API 一致性:从可用性角度而言,QCanvasPath 与 QCanvasPainter 共享 1:1 的路径绘制 API 至关重要,使直接绘制与路径绘制之间的代码切换更为便捷。此外,我们也希望 API 与 HTML canvas Path2D 保持一致。

  2. 数据格式:QCanvasPath 的内存占用通常比 QPainterPath 少约 60%。内部数据格式对 CPU 缓存也更为友好(SoA vs. AoS)。由于路径数据格式与 QCanvasPainter 完全一致,路径的 add / fill / stroke 操作可直接通过 memcpy 完成,效率极高。

  3. 缓存:由于对路径拥有完全控制权,构建高效缓存策略也更为容易。路径不变时,可直接复用数据,因为数据格式是共享的。此外,还可进行 GPU 侧缓存——静态路径可持久保存在顶点缓冲区中,重绘操作的代价极低。

 

了解了 QCanvasPath 的基础知识后,我们来深入探讨路径组及其对性能的影响。

路径组

QCanvasPath 路径组是一种将相关路径归为一组的机制。这是 HTML canvas 2d context 和 QPainter & QPainterPath 均不支持的全新特性。Qt Canvas Painter 原生依托 QRhi 实现硬件加速,因此我们决定实现这一功能。在底层,单个路径组对应单个顶点缓冲区,因此每个组应包含需要同时更新的路径,以确保每帧更新的顶点缓冲区数量和顶点数量都保持最优。

路径组仅作为 stroke()fill() 方法的一个额外整数参数,原因如下:

  • 尽可能保持 API 与 HTML canvas 2d context API 的一致性。这只是这些方法的一个可选额外参数,在需要进行额外优化时使用。

  • 提供分组的灵活性,因为需求因项目而异,并可能在应用生命周期内发生变化。路径组可作为 C++ 枚举处理,例如命名为 STATIC_PATHS、DYNAMIC_PATHS、ICON_PATHS、SHARK_BACKGROUND_PATHS,或任何 UI 所需的名称。

  • 与即将推出的 Canvas2D QML 元素良好配合。更多详情稍后介绍。

Qt Canvas Painter 会在必要时自动使路径失效。相关内容有详尽文档说明,简而言之,当顶点需要更新时路径需重新生成,例如描边线宽发生变化或端点/连接类型改变时。因此,为获得最佳性能,应避免对已缓存路径进行这类修改。同样重要的是了解哪些操作不会使路径失效。例如,路径的变换操作(平移、旋转、缩放)在 GPU 上完成,速度极快;更改填充/描边画刷也不会使路径失效,因此颜色和渐变动画同样非常流畅。以下视频展示了路径测试示例应用的实际效果:

视频中可以清晰看到:当线段数量增至 100 万条时,直接绘制和非缓存路径绘制均如预期般开始出现卡顿。然而,开启路径缓存后,即便是 100 万条线段也能流畅渲染。无论是对路径进行变换还是对画刷进行动画处理,性能均不受影响,因为这些操作无需更新顶点缓冲区。

渐进式渲染

至此,我们了解到,对于每帧不发生变化的路径,可通过 QCanvasPath 绘制来提升性能。而对于路径持续动画的 UI,尤其是在针对嵌入式设备进行优化时,同样可以从 QCanvasPath 中获益。

此处的"渐进式渲染"是指将渲染工作负载分散到不同帧中,以实现更好的整体性能并降低 CPU & GPU 消耗。在传统 60Hz 屏幕上,单帧有 16ms 的时间来显示新渲染的内容。若超过此时间,帧将被丢弃,无法达到满帧同步刷新率。一种解决方案是将屏幕刷新率降至一半,但如果 UI 中某些部分需要完整的 60Hz 动画,这并非最优选择。

Qt Quick 为渐进式渲染提供的一个通用方案是使用 item 图层,例如通过 FrameAnimation 在奇/偶帧上将单个帧设为 live。这需要一些手动动画工作和图层纹理缓存,但也是一种有效方案。使用 QCanvasPainterItem 则更为简便,因为该 item 隐式由纹理支撑,只需在内容真正需要更新时调用 item 的 update() 即可。但这些方案仅在整个 item 需要渐进式渲染时有效,对于 item 中部分区域需持续更新、其他区域更新频率较低的情况则无能为力。

QCanvasPath 和路径组提供了另一种渐进式渲染方式:缓存路径的顶点缓冲区而非图层纹理。这样可以实现更细粒度的控制,只缓存 item 的部分内容。以下以一个适合医疗/工业场景的示例 UI 来说明:

qcanvaspainter_medicalprogressivepath_example

该 UI 包含 4 个图表组件,每个组件均有多条实时绘制的折线图。目标硬件为嵌入式设备,无法以满屏刷新率渲染所有图表。同时,UI 右侧还包含一个计时器组件,需要高精度并以全速更新。较粗的主图表也需要全速更新,但其他较细的图表实际上不需要那么频繁地更新。传感器/数据库提供数据的速度可能本身也没那么快,每秒更新 15 次(15Hz)即已足够。此外,降低 CPU & GPU 负载也有助于延长电池续航。

为实现这一目标,我们使用 QCanvasPath,将每个图表 item 绘制到各自的路径组中,并通过如下代码实现每帧仅更新单个图表:

...
m_frame++;
m_update = (m_frame % 4 == m_graphIndex);
if (m_update) {
  m_path.clear();
.. recreate the graph ..
}
...


经过这些相当简单的改动,开启渐进式渲染后,UI 实现了更低的 CPU & GPU 占用,以及整体稳定的 60fps。以下视频展示了在低端 Android 平板(Lenovo Tab M10 HD,Mediatek MT6762 + PowerVR GE8320)上的实际表现:

总结

如果您还未体验过 Qt Canvas Painter,现在正是时候。Qt 6.12.0 Beta 1 近期已正式发布,Qt Canvas Painter 将在 Qt 6.12 中升级为完整支持的模块,现在进行测试并提供反馈将大有裨益。此外,Qt 6.12 还将引入全新的 Canvas2D QML 元素,将 Canvas Painter 的流畅体验带入 QML JavaScript 世界。更多详情敬请期待,祝大家 Happy hacking,夏日愉快!

 

Comments

Subscribe to our blog

Try Qt 6.11 Now!

Download the latest release here: www.qt.io/download

Qt 6.11 is now available, with new features and improvements for application developers and device creators.

We're Hiring

Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.