Canvas与自定义渲染
# 第 9 章 Canvas 与自定义渲染
本章定位:当标准控件不够用时。前面 8 章教你用 Rectangle/Text/ListView 这些 QML 标准控件——它们底层走 Scene Graph 批处理、性能很好。但现在你遇到一个实时心电波形、一个复杂路径图表、或一个需要逐像素操作的渲染场景——标准控件做不到了。本章讲三条自定义渲染路径:Canvas 2D 绘制引擎、QQuickPaintedItem 命令式 QPainter、以及 QQuickItem + QSGNode 零拷贝 GPU 渲染——它们的原理、性能差距与选择策略。
# 目录介绍
- 9.1 案例引入
- 9.2 Canvas 引擎
- 9.3 Shape 矢量路径
- 9.4 Painted 命令式
- 9.5 自定义节点
- 9.6 实时心电
- 9.7 路径选型
- 9.8 新手陷阱
- 9.9 训练题
- 9.10 速查表
# 9.1 案例引入
# 9.1.1 心电图掉帧
某医疗设备工程师用 QML Canvas 画实时心电波形——200Hz 采样率的数据流。在 x86 桌面跑得很流畅,部署到 ARM Cortex-A53 板上后波形从 30fps 骤降到 5fps:
import QtQuick
Canvas {
id: ekgCanvas
width: 600; height: 300
property var dataPoints: []
property int maxPoints: 500
function pushSample(value) {
dataPoints.push(value)
if (dataPoints.length > maxPoints) dataPoints.shift()
requestPaint() // ⚠️ 每 5ms 一次 → Canvas 重绘 → 爆炸
}
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height) // ← 每次重绘先清空
ctx.strokeStyle = "#00ff00"
ctx.lineWidth = 2
ctx.beginPath()
for (var i = 0; i < dataPoints.length; i++) { // ← 500 个点到 JS 循环
var x = i * width / maxPoints
var y = height / 2 - dataPoints[i] * 100
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
}
测得现象:
| 指标 | x86 桌面 | ARM A53 |
|---|---|---|
| 波形刷新率 | 30 fps | 5 fps |
| CPU 占用 | 15% | 95%+ |
| 每帧耗时 | ~2ms | ~180ms |
| JS 循环开销 | V4 JIT 优化后很低 | V4 JIT 在 ARM 上弱很多 |
疑惑:
- Canvas 不是硬件加速的吗?为什么比 Rectangle 慢这么多?
requestPaint()到底走了什么路径?和 Scene Graph 批处理有关吗?- 有没有不走 JS 循环的绘制方案?
# 9.1.2 根因分析
每次 requestPaint() 触发 Canvas 完整重绘流程:
requestPaint() → Canvas 标记为 dirty
→ Render Thread Sync 阶段:
① Canvas::updatePaintNode()
→ 创建一个 FBO(Frame Buffer Object,离屏缓冲)
→ 调用 V4 引擎执行 onPaint 里的 JS 代码 ← 在 GUI 线程!
→ V4 执行 Context2D 命令:
beginPath / moveTo / lineTo × 500 / stroke
→ 生成 500 个顶点的路径
→ CPU 光栅化到 FBO
→ FBO → 纹理上传(glTexImage2D) ← 又慢又占带宽
② 纹理作为 QSGTextureMaterial 挂到 QSGGeometryNode
③ Scene Graph 渲染这个纹理
关键:整个 onPaint 里的 JS + CPU 光栅化都在 GUI 线程的 Sync 阶段执行!
这意味着 Scene Graph 的 Sync 阶段从 1ms 膨胀到 180ms。
修复方案——换 QQuickItem + QSGNode:
- 数据在 C++ 侧直接写入
QSGGeometry的顶点缓冲 - 不经过 V4 JS、不经过 FBO → 纹理上传
- Scene Graph 批处理直接渲染这些顶点 → 零拷贝路径
- 200Hz 数据流 → 60fps 波形刷新
# 9.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
Canvas 的 requestPaint() 到底走了什么渲染路径?为什么慢? | §9.2 |
| QQuickPaintedItem 和 Canvas 的性能差别在哪?什么时候选前者? | §9.4 |
| 零拷贝 QSGNode 路径具体怎么写?200Hz 数据不丢帧? | §9.5 + §9.6 |
读完本章你会把 ARM 板上的心电波形从 5fps 优化到 60fps,并亲手写一个 QQuickItem + QSGNode 零拷贝渲染节点。
# 9.2 Canvas 引擎
# 9.2.1 API 全景
QML Canvas 的 API 与 HTML5 Canvas 几乎一致——Context2D 提供 2D 绘图的完整接口:
Canvas {
width: 400; height: 300
id: canvas
onPaint: {
var ctx = getContext("2d")
// ───── 填充与描边 ─────
ctx.fillStyle = "steelblue"
ctx.fillRect(20, 20, 100, 50)
ctx.strokeStyle = "red"
ctx.lineWidth = 2
ctx.strokeRect(150, 20, 100, 50)
// ───── 路径绘制 ─────
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.lineTo(100, 200)
ctx.lineTo(200, 150)
ctx.closePath() // 闭合路径
ctx.fillStyle = "#4CAF50"
ctx.fill() // 填充闭合区域
// ───── 圆形与弧线 ─────
ctx.beginPath()
ctx.arc(300, 100, 40, 0, Math.PI * 2)
ctx.strokeStyle = "orange"
ctx.stroke()
// ───── 文字 ─────
ctx.font = "16px sans-serif"
ctx.fillStyle = "black"
ctx.fillText("Hello Canvas", 20, 280)
// ───── 变换 ─────
ctx.save()
ctx.translate(300, 250)
ctx.rotate(Math.PI / 6)
ctx.fillStyle = "purple"
ctx.fillRect(-30, -15, 60, 30)
ctx.restore() // 恢复变换矩阵
// ───── 图片 ─────
ctx.drawImage("qrc:/icon.png", 10, 10) // 简单绘制
ctx.drawImage("qrc:/sprite.png", 0,0,64,64, 100,100,64,64) // 裁剪区域
}
// 手动触发重绘
property real data: 0
onDataChanged: requestPaint()
}
Context2D 的状态机机制——save() / restore() 维护一个状态栈:
ctx.save() → 当前变换矩阵 + 描边样式 + 填充样式 + 字体 全部入栈
ctx.restore() → 弹出栈顶 ← 回退到 save() 之前的状态
这个机制让嵌套变换(如刻度盘上的指针旋转后回归正常坐标)变得简单。
# 9.2.2 FBO 缓冲
Canvas 每次 onPaint 执行的完整链路:
GUI Thread (在 Sync 阶段被 Render Thread 短暂阻塞):
① QQmlEngine 调用 V4 引擎 → 执行 onPaint 内的 JS 代码
② V4 执行 Context2D 指令(路径、填充、文字...)
③ CPU 光栅化到 FBO(GPixel 逐像素计算)
④ FBO 内容 → glTexImage2D → GPU 纹理(显存)
Render Thread:
⑤ 纹理作为 QSGTextureMaterial 挂到 QSGGeometryNode
⑥ Scene Graph 批处理渲染此节点
⑦ eglSwapBuffers
FBO 开销的量化对比:
| 操作 | 标准 QML 控件 | Canvas |
|---|---|---|
| 数据到渲染 | QML 属性 → Scene Graph 顶点绑定 | JS for 循环 → Context2D → CPU 光栅化 |
| GPU 路径 | 直接:QSGGeometryNode | 间接:FBO → 纹理 → QSGGeometryNode |
| CPU 参与度 | 几乎为零 | 100% 参与 FBO 光栅化 |
| 批处理 | ✅ 全自动 | ❌ 独立 FBO → 无法批处理 |
requestPaint() 的两种模式:
| 模式 | 触发方式 | 重绘范围 | 性能 |
|---|---|---|---|
requestPaint() | 异步,下一帧 Sync 前合并 | 整个 Canvas | 多次调用合并为一次 |
requestPaint(rect) | 指定脏区域 | 仅 rect 区域 | 小范围更新更快 |
// 仅重绘变化部分——减小 FBO 光栅化面积
onDataChanged: canvas.requestPaint(Qt.rect(200, 0, 100, 300))
# 9.2.3 性能与缓存
Canvas 的性能临界点:
| 场景 | 帧率 | 建议 |
|---|---|---|
| 静态图表(柱状图、饼图) | 按需刷新 | ✅ Canvas 首选 |
| 低频动画(1-2fps) | 2fps 稳定 | ✅ Canvas 可用 |
| 中频更新(10fps) | 10fps → CPU 60% | ⚠️ 简化 onPaint 逻辑 |
| 高频更新(30fps+) | 30fps → CPU 95% | ❌ 换 QQuickItem + QSGNode |
Canvas 缓存策略:
Canvas {
renderStrategy: Canvas.Cooperative // 默认:与 Scene Graph 协同
// renderStrategy: Canvas.Immediate // 立即渲染——阻塞性,仅调试用
// renderStrategy: Canvas.Threaded // Qt 5.10+ 独立线程渲染——实验性
}
# 9.2.4 频谱波形
import QtQuick
ApplicationWindow {
width: 600; height: 200; visible: true
Canvas {
id: spectrum
anchors.fill: parent
property var frequencies: []
onPaint: {
var ctx = getContext("2d")
var w = width; var h = height
ctx.clearRect(0, 0, w, h)
if (!frequencies.length) return
var barW = w / frequencies.length
for (var i = 0; i < frequencies.length; i++) {
var barH = frequencies[i] * h
var hue = (i / frequencies.length) * 360
ctx.fillStyle = "hsl(" + hue + ", 80%, 60%)"
ctx.fillRect(i * barW, h - barH, barW - 2, barH)
}
}
// 定时更新数据
Timer {
interval: 50; repeat: true; running: true
onTriggered: {
var data = []
for (var i = 0; i < 64; i++)
data.push(Math.random())
spectrum.frequencies = data
spectrum.requestPaint() // 每次 Timer tick 触发重绘
}
}
}
}
案例知识融合:本案例演示了 Canvas 的标准使用模式——①Timer 定时更新数据(非高频,50ms 间隔);②frequencies 变化触发 requestPaint();③onPaint 里纯 Context2D 绘制 64 个矩形条。这个场景 Canvas 完全够用——更新频率 ≤ 20fps + 绘制元素 ≤ 100。
思考题:
- 如果把
Timer.interval: 50改成interval: 5(200fps),在 ARM 板上 Canvas 能撑住吗?瓶颈在 FBO 光栅化还是 V4 JS 执行? ctx.fillStyle = "hsl(" + hue + "..."每次循环都做字符串拼接——改成预计算颜色数组会提升多少性能?
# 9.3 Shape 矢量路径
# 9.3.1 矢量渲染
Qt 6 引入了 Shape + ShapePath——利用 GPU 的曲面细分(Tessellation) 直接渲染 SVG 路径,不经过 FBO:
import QtQuick.Shapes
Shape {
width: 200; height: 200
ShapePath {
strokeColor: "#FF5252"; strokeWidth: 2
fillColor: "#4CAF50"
fillRule: ShapePath.WindingFill // 填充规则
startX: 100; startY: 10
PathLine { x: 190; y: 180 }
PathLine { x: 10; y: 180 }
PathLine { x: 100; y: 10 }
// 或使用 SVG 路径字符串
// PathSvg { path: "M 100 10 L 190 180 L 10 180 Z" }
}
}
Shape 的渲染路径 vs Canvas:
Shape:
PathLine / PathQuad / PathCubic
→ GPU 曲面细分 → 三角形网格 → QSGGeometryNode
→ Scene Graph 批处理 → 直接渲染 ← 无 FBO
Canvas:
PathLine / PathQuad / PathCubic(Context2D)
→ CPU 光栅化到 FBO → 纹理上传 → QSGGeometryNode
→ Scene Graph 渲染 ← 有 FBO
# 9.3.2 Shape 对比
| 维度 | Canvas | Shape |
|---|---|---|
| 路径渲染 | CPU 光栅化 → FBO | GPU 曲面细分 |
| 批处理兼容 | ❌ 独立 FBO | ✅ 同批处理 |
| 动态路径 | requestPaint() 全刷新 | 属性绑定自动更新 |
| 填充规则 | nonzero / evenodd | WindingFill / OddEvenFill |
| 渐变色 | createLinearGradient() | LinearGradient {} 对象 |
| 嵌入式 GPU 占用 | CPU 密集 | GPU 密集——在 ARM Mali 上更快 |
# 9.4 Painted 命令式
# 9.4.1 QPainter 路径
QQuickPaintedItem 适用于从 Qt Widget 的 QPainter 代码迁移:
class ChartItem : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(QVector<double> data READ data WRITE setData NOTIFY dataChanged)
public:
void paint(QPainter* painter) override {
painter->setRenderHint(QPainter::Antialiasing);
painter->fillRect(boundingRect(), QColor("#1a1a2e"));
QPen pen(QColor("#4CAF50"), 2);
painter->setPen(pen);
QPainterPath path;
for (int i = 0; i < m_data.size(); ++i) {
double x = i * width() / m_data.size();
double y = height() / 2 - m_data[i] * 100;
if (i == 0) path.moveTo(x, y);
else path.lineTo(x, y);
}
painter->drawPath(path);
}
private:
QVector<double> m_data;
};
ChartItem {
id: chart
anchors.fill: parent
data: [0.1, 0.3, 0.5, ...]
}
渲染路径:paint(QPainter*) → QPainter 引擎(C++,非 JS)→ 输出到 QImage(FBO)→ glTexImage2D 上传 GPU。
QQuickPaintedItem vs Canvas:
| 维度 | Canvas | QQuickPaintedItem |
|---|---|---|
| 语言 | V4 JS | C++ |
| CPU 光栅化速度 | 慢(V4 路径) | 快 3-5×(QPainter C++ 路径) |
| FBO → 纹理 | 同样走 | 同样走 |
| 异步渲染 | renderStrategy: Threaded | setPerformanceHint(FastFBOResizing) |
| 适用场景 | QML 全栈开发者 | 有 Qt Widget 存量代码 |
# 9.4.2 Widget 迁移
| 原 Widget 代码 | QQuickPaintedItem 等价 |
|---|---|
QWidget::paintEvent(QPaintEvent*) | QQuickPaintedItem::paint(QPainter*) |
painter->drawRect(QRect(10,10,100,50)) | 完全相同——QPainter API 不变 |
QWidget::update() | QQuickPaintedItem::update() |
QWidget::setFixedSize() | QQuickPaintedItem::setSize(QSizeF()) |
# 9.5 自定义节点
# 9.5.1 零拷贝路径
这是 QML 自定义渲染的性能天花板——数据直接从 C++ 写入 GPU 顶点缓冲,不经过任何 JS、FBO、纹理上传:
class WaveItem : public QQuickItem {
Q_OBJECT
static constexpr int MAX_POINTS = 500;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
// ① 复用或创建 QSGGeometryNode
auto* node = static_cast<QSGGeometryNode*>(oldNode);
if (!node) {
node = new QSGGeometryNode();
// ② 分配顶点缓冲——GL_LINE_STRIP 模式
auto* geometry = new QSGGeometry(
QSGGeometry::defaultAttributes_Point2D(), MAX_POINTS);
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry); // node 负责释放 geometry
// ③ 设置材质
auto* material = new QSGFlatColorMaterial();
material->setColor(QColor("#00ff00"));
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
// ④ 直接写入顶点数据——零拷贝!
auto* geometry = node->geometry();
geometry->allocate(m_pointCount); // 按实际点数分配
auto* vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < m_pointCount; ++i) {
vertices[i].set(
i * width() / MAX_POINTS,
height() / 2 - m_buffer[i] * height() * 0.4
);
}
// ⑤ 标记为 dirty——下一帧 Sync 阶段被 Scene Graph 拾取
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
void pushSample(float value) {
m_buffer[m_pointCount % MAX_POINTS] = value;
if (m_pointCount < MAX_POINTS) ++m_pointCount;
update(); // ← 标记 QQuickItem 为 dirty → 触发 updatePaintNode
}
private:
float m_buffer[MAX_POINTS] = {};
int m_pointCount = 0;
};
# 9.5.2 顶点缓冲
QSGGeometry 的绘制模式与顶点属性:
| 绘制模式 | 表示 | 例子 |
|---|---|---|
DrawPoints | 独立点 | 散点图 |
DrawLines | 每两个顶点一条线段 | 网格线 |
DrawLineStrip | 连续折线 | 心电波形 |
DrawTriangles | 每三个顶点一个三角形 | 填充区域 |
DrawTriangleStrip | 连续三角形带 | 带状图 |
顶点属性集选择:
// 二维坐标(x, y)
QSGGeometry::defaultAttributes_Point2D() // → 两个 float
// 二维坐标 + 颜色(x, y, r, g, b, a)
QSGGeometry::defaultAttributes_ColoredPoint2D()
// 二维坐标 + 纹理坐标
QSGGeometry::defaultAttributes_TexturedPoint2D()
# 9.5.3 脏标同步
updatePaintNode() 在 Render Thread 的 Sync 阶段被调用——这是两线程唯一的交汇点:
数据写入路径(任意线程 → GUI 线程 → Render Thread):
传感器线程: pushSample(value)
→ 锁保护写 m_buffer ← 可以来自任何线程
→ QQuickItem::update() ← 必须在 GUI 线程调用!
使用 signal/slot 桥接:
connect(sensor, &Sensor::newSample, waveItem, [=](float v) {
waveItem->pushSample(v); // ← Qt::AutoConnection 保证在 GUI 线程
});
// 线程安全的环形缓冲
class WaveItem : public QQuickItem {
Q_OBJECT
public slots:
void pushSample(float value) {
QMutexLocker lock(&m_mutex);
m_ringBuffer[m_writeIndex % MAX_POINTS] = value;
++m_writeIndex;
m_hasNewData = true;
lock.unlock();
update(); // ← 必须在 GUI 线程
}
private:
QMutex m_mutex;
std::atomic<bool> m_hasNewData{false};
int m_writeIndex = 0;
};
# 9.6 实时心电
// EKGItem.h + EKGItem.cpp
class EKGItem : public QQuickItem {
Q_OBJECT
static constexpr int MAX_POINTS = 1024;
QSGNode* updatePaintNode(QSGNode* old, UpdatePaintNodeData*) override {
auto* node = static_cast<QSGGeometryNode*>(old);
if (!node) {
node = new QSGGeometryNode();
auto* geom = new QSGGeometry(
QSGGeometry::defaultAttributes_Point2D(), MAX_POINTS);
geom->setDrawingMode(QSGGeometry::DrawLineStrip);
node->setGeometry(geom);
node->setFlag(QSGNode::OwnsGeometry);
auto* mat = new QSGFlatColorMaterial();
mat->setColor(QColor("#00ff00"));
node->setMaterial(mat);
node->setFlag(QSGNode::OwnsMaterial);
// 背景网格——独立 QSGGeometryNode
auto* gridNode = createGridNode();
node->appendChildNode(gridNode);
}
// 更新波形顶点
QMutexLocker lock(&m_mutex);
auto* geom = node->geometry();
geom->allocate(m_pointCount);
auto* verts = geom->vertexDataAsPoint2D();
float w = width(), h = height();
for (int i = 0; i < m_pointCount; ++i)
verts[i].set(i * w / MAX_POINTS, h/2 - m_buffer[i] * h * 0.4);
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
QSGGeometryNode* createGridNode() {
// 水平网格线——简化实现
auto* node = new QSGGeometryNode();
// ...
return node;
}
public slots:
void pushSample(float value) {
QMutexLocker lock(&m_mutex);
m_buffer[m_pointCount % MAX_POINTS] = value;
if (m_pointCount < MAX_POINTS) ++m_pointCount;
lock.unlock();
update();
}
private:
QMutex m_mutex;
float m_buffer[MAX_POINTS] = {};
int m_pointCount = 0;
};
import QtQuick
ApplicationWindow {
width: 800; height: 400; visible: true; color: "#0a0a1a"
EKGItem {
id: ekg
anchors.fill: parent
anchors.margins: 20
}
Timer {
interval: 5; repeat: true; running: true // 200Hz 模拟
onTriggered: {
ekg.pushSample(Math.sin(Date.now() / 200) * 0.8
+ Math.sin(Date.now() / 87) * 0.3
+ (Math.random() - 0.5) * 0.1)
}
}
}
案例知识融合:本案例集成了 QSGNode 零拷贝渲染的完整链路——①C++ pushSample 写入环形缓冲(线程安全);②update() 标记 dirty;③updatePaintNode 直接写顶点缓冲(零拷贝);④Scene Graph 在批处理阶段渲染波形。ARM A53 上实测 60fps 稳定,CPU 占用 < 15%。
# 9.7 路径选型
| 路径 | 性能 | CPU 路径 | 批处理 | 适合场景 | 开发复杂度 |
|---|---|---|---|---|---|
| Canvas | ★★ | V4 JS → FBO | ❌ | 低频图表、仪表盘 | ★ 低 |
| QQuickPaintedItem | ★★★ | C++ QPainter → FBO | ❌ | Widget 迁移、复杂路径 | ★★ 中 |
| Shape | ★★★★ | GPU 曲面细分 | ✅ | 矢量路径、SVG 图标 | ★★ 中 |
| QQuickItem + QSGNode | ★★★★★ | C++ 顶点缓冲 | ✅ | 高频实时数据渲染 | ★★★★ 高 |
选型决策:
更新频率 < 5fps?
├─ 是 → Canvas
└─ 否 → 有 Qt Widget 存量代码?
├─ 是 → QQuickPaintedItem
└─ 否 → 更新频率 > 30fps?
├─ 是 → QQuickItem + QSGNode
└─ 否 → Shape(矢量)/ Canvas(像素操作)
# 9.8 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | Canvas 高频 requestPaint() | 每帧 FBO→纹理→GPU 全路径 | 换 QSGNode;或合并为批量更新 |
| 2 | updatePaintNode 里没设 OwnsGeometry | 内存泄漏——每次新 geometry 不释放 | node->setFlag(QSGNode::OwnsGeometry) |
| 3 | sensor 线程直接调 update() | QSGRenderThread safety 违反 | 通过信号桥接 Qt::AutoConnection |
| 4 | Canvas JS 循环里字符串拼接 | 每帧重做字符串操作 | 预计算、或换 C++ 方案 |
| 5 | Shape 用太复杂路径 | GPU 曲面细分顶点爆炸 | 简化路径点、关闭不必要的 fillRule |
# 9.9 训练题
将 Canvas 心电波形改为 QSGNode 方案
下面的 Canvas 代码在 ARM 板上只能跑 5fps。写出等效的 QQuickItem + QSGNode 实现:
Canvas {
property var data: []
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0,0,width,height)
ctx.beginPath()
for (var i = 0; i < data.length; i++) {
var x = i * width / 500; var y = height/2 - data[i] * 100
i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y)
}
ctx.stroke()
}
}
参考答案
参见 §9.6 的 EKGItem 实现——updatePaintNode 里 QSGGeometry::DrawLineStrip 直接写顶点数据,绕过 Canvas 的 FBO 路径。
# 9.10 速查表
| 概念 | 一句话 |
|---|---|
| Canvas | V4 JS 执行 Context2D → FBO 离屏缓冲 → 纹理上传 |
| requestPaint() | 标记 Canvas dirty——下一帧 Sync 合并执行一次 onPaint |
| FBO | 离屏帧缓冲——Canvas/QQuickPaintedItem 的中间渲染目标 |
| Shape | GPU 曲面细分矢量路径——无 FBO、可批处理 |
| QQuickPaintedItem | C++ QPainter → FBO——适合 Widget 迁移 |
| QSGGeometryNode | Scene Graph 核心绘制节点——顶点 + 材质 |
| QSGGeometry | 顶点缓冲——DrawPoints/DrawLines/DrawLineStrip/DrawTriangles |
| updatePaintNode() | 每帧 Sync 阶段调用——在此写入顶点数据 |
| markDirty | 标记节点需要更新——DirtyGeometry/DirtyMaterial/DirtyMatrix |
| OwnsGeometry | QSGNode 标志——节点析构时自动释放 geometry 内存 |
| zero-copy | 数据直写顶点缓冲 → 无 JS、无 FBO、无纹理上传 |
核心哲学:
Canvas = "给我 JS,我画一张位图贴到屏幕"。
QSGNode = "给我顶点数据,我让 GPU 直接画"。
前者开发快(5 行代码出图),后者性能高(200Hz 不掉帧)。
选型不看"谁更高级"——看你需要的是开发速度还是运行速度。