如果您尚未了解全新的 Qt Canvas Painter,欢迎参阅此前的博客文章:简介、新功能和性能测试。本文将介绍路径(path)和路径组(path group),以进一步提升性能。
硬件加速渲染领域有一个普遍认知:命令式绘制速度慢,声明式绘制速度快。其背后的基本逻辑是,GPU 擅长持有数据并进行并行处理,而从 CPU 向 GPU 传输数据可能成为并行处理的瓶颈。声明式 UI 方法采用场景图(scene graph)方式,可追踪 UI 中实际需要更新的部分并最小化更新量,从而提升性能。
这一逻辑站得住脚,但我认为命令式绘制并非必然缓慢。毕竟,Vulkan、Metal 等底层图形 API 既是命令式的,又拥有极高性能。我倾向于用直接绘制来描述直接在屏幕上绘制的方式,而命令式绘制也可以面向屏幕以外的其他缓冲区。Qt Canvas Painter 的直接绘制已经过深度优化,性能极为出色,同时我们还提供了 QCanvasPath 类用于管理路径。
先从一个简单示例入手——绘制如下自定义折线图:

使用 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);
观察代码,可以发现几个有趣之处:
-
绘制代码高度相似。这是有意为之——QCanvasPath 与 QCanvasPainter 共享完全相同的绘制 API,使直接绘制与路径绘制之间的代码迁移变得轻松。
-
路径仅在第一帧为空,后续帧将完全跳过 for 循环,直接绘制已有路径。
-
stroke 方法中有一个神秘的数字参数,这是用于缓存顶点缓冲区(vertex buffer)的可选路径组参数,可进一步提升性能。
路径方式的另一大优势在于,只需添加如下几行代码..
...
p->setLineWidth(20);
p->setStrokeStyle(Qt::black);
p->setAntialias(10);
p->translate(-2, 4);
p->stroke(m_path, 1);
...
……即可复用同一路径,以极低的额外开销添加平滑阴影效果:

QCanvasPath vs. QPainterPath
有人或许会问:Canvas Painter 为什么不直接使用现有的 QPainterPath 类?这是一个合理的问题,值得认真解答。首先,Canvas Painter 确实大量使用了 QtGui 的工具类(QColor、QFont、QImage、QTransform、QRectF 等),因此原因并非标新立异。以下是自建路径类的三大核心原因:
-
API 一致性:从可用性角度而言,QCanvasPath 与 QCanvasPainter 共享 1:1 的路径绘制 API 至关重要,使直接绘制与路径绘制之间的代码切换更为便捷。此外,我们也希望 API 与 HTML canvas Path2D 保持一致。
-
数据格式:QCanvasPath 的内存占用通常比 QPainterPath 少约 60%。内部数据格式对 CPU 缓存也更为友好(SoA vs. AoS)。由于路径数据格式与 QCanvasPainter 完全一致,路径的 add / fill / stroke 操作可直接通过 memcpy 完成,效率极高。
-
缓存:由于对路径拥有完全控制权,构建高效缓存策略也更为容易。路径不变时,可直接复用数据,因为数据格式是共享的。此外,还可进行 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 来说明:

该 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,夏日愉快!