自定义SceneGraph节点
# 第 11 章 自定义 SceneGraph 节点
本章定位:直接操控 GPU 渲染的最后一层。前面第 9 章讲了三条自定义渲染路径——Canvas(JS → FBO)、QQuickPaintedItem(QPainter → FBO)、QQuickItem+QSGNode(顶点缓冲)。本章深入第三条路径的最深处:自定义
QSGMaterial材质系统、自写着色器(Vertex/Fragment Shader)、以及摄像头帧的零拷贝纹理上传——这是 QML 渲染性能的天花板,也是嵌入式上跑 60fps 复杂视觉效果的唯一解。
# 目录介绍
- 11.1 案例引入
- 11.2 节点体系
- 11.3 自定义全流程
- 11.4 材质着色器
- 11.5 零拷贝纹理
- 11.6 粒子系统
- 11.7 线程与脏标
- 11.8 新手陷阱
- 11.9 训练题
- 11.10 速查表
# 11.1 案例引入
# 11.1.1 预览掉帧
某车载工程师用 VideoOutput 显示后视摄像头画面——ARM Cortex-A53 + Mali-G31 板上预览帧率 8fps,延迟超过 200ms:
VideoOutput {
source: camera
anchors.fill: parent
}
排查发现 VideoOutput 内部的渲染路径:
摄像头硬件 → V4L2 驱动 → 内核 DMA buffer
→ Qt Multimedia → QVideoFrame
→ QVideoFrame::toImage() ⚠️ CPU 拷贝!
→ glTexImage2D(QImage) ⚠️ 纹理上传!
→ QSGGeometryNode 渲染
每一帧都经历了 GPU → CPU(map 内存)→ CPU(格式转换)→ CPU → GPU(texImage2D) 四次跨边界拷贝。
# 11.1.2 根因分析
修复方案——绕过 VideoOutput,直接用 QQuickItem + createTextureFromNativeObject 把 DMA buffer 的 GPU 纹理句柄传给 Scene Graph:
摄像头硬件 → V4L2 → DMA buffer
→ eglCreateImageKHR → GLuint texture ← 零 CPU 拷贝!直接 GPU 纹理
→ window()->createTextureFromNativeObject(nativeHandle)
→ QSGTextureMaterial → QSGGeometryNode → 渲染
结果:ARM 板上 8fps → 30fps(摄像头硬件帧率上限),延迟从 200ms 降到 < 16ms。
# 11.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
QSGGeometryNode + QSGFlatColorMaterial 之外,自定义材质怎么写? | §11.4 |
| GPU 纹理句柄如何不经过 CPU 直接传给 QML? | §11.5 |
| 自定义节点在 Render Thread 内如何保证线程安全? | §11.7 |
# 11.2 节点体系
# 11.2.1 类型生命周期
QSGNode ── 所有节点的基类
├── QSGTransformNode ── 变换矩阵(translate/rotate/scale)
├── QSGOpacityNode ── 透明度
├── QSGClipNode ── 裁剪(scissor test)
└── QSGGeometryNode ── 核心绘制节点(顶点缓冲 + 材质)
├── QSGGeometry ── 顶点数据(坐标/颜色/纹理坐标)
└── QSGMaterial ── 材质(着色器 + uniform + 纹理)
节点的创建与销毁:
// updatePaintNode 被 Render Thread 的 Sync 阶段调用
QSGNode* MyItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
auto* node = static_cast<QSGGeometryNode*>(oldNode);
// ① 首次调用:oldNode == nullptr → 创建新节点
if (!node) {
node = new QSGGeometryNode();
auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 100);
node->setGeometry(geom);
node->setFlag(QSGNode::OwnsGeometry); // node 析构时释放 geometry
auto* mat = new QSGFlatColorMaterial();
mat->setColor(Qt::green);
node->setMaterial(mat);
node->setFlag(QSGNode::OwnsMaterial); // node 析构时释放 material
}
// ② 后续调用:复用 oldNode,只更新数据——避免每帧 new/delete
auto* geom = node->geometry();
updateVertices(geom->vertexDataAsPoint2D());
// ③ 标记脏位——告知 Scene Graph 需要更新什么
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
OwnsGeometry / OwnsMaterial 的含义——当 QSGNode 被 Scene Graph 销毁时,自动释放挂载的 geometry 和 material:
| 标志 | 效果 |
|---|---|
OwnsGeometry | node 析构时 delete geometry |
OwnsMaterial | node 析构时 delete material |
OwnedByParent | node 被父节点析构时一并销毁 |
UsePreprocess | 本节点的 preprocess() 会被调用 |
# 11.2.2 子节点挂载
QSGNode 可以有子节点——形成渲染树:
QSGNode* MyItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
auto* root = static_cast<QSGNode*>(oldNode);
if (!root) {
root = new QSGNode(); // 根——无视觉
auto* bg = new QSGGeometryNode(); // 背景矩形
// ... 设置 bg 的 geometry + material
root->appendChildNode(bg);
auto* label = new QSGGeometryNode(); // 文字标签
// ... 设置 label 的 geometry + material
root->appendChildNode(label);
}
return root;
}
节点层级与渲染顺序:
Root (QSGNode)
├── Background (QSGGeometryNode) ← 先画
├── GridLines (QSGGeometryNode) ← 再画
├── DataCurve (QSGGeometryNode) ← 再画
└── Label (QSGGeometryNode) ← 最后画(最顶层)
Scene Graph 在 Render Phase 按前序遍历渲染——
不依赖 z 值(那是 QML Item 层面的东西)。
# 11.2.3 节点变换
如果节点需要在渲染前计算变换矩阵(如动态缩放),重写 preprocess():
class ScaledNode : public QSGGeometryNode {
public:
void preprocess() override {
// 在 Render Phase 之前——计算变换矩阵
QMatrix4x4 m;
m.scale(m_scale);
setMatrix(m);
QSGGeometryNode::preprocess(); // 调用父类
}
void setScale(float s) {
if (qFuzzyCompare(s, m_scale)) return;
m_scale = s;
markDirty(QSGNode::DirtyMatrix);
}
private:
float m_scale = 1.0f;
};
# 11.2.4 渲染四阶段
updatePaintNode 并非每帧调用一次——它在 Scene Graph 内部时序有固定的"入口窗口":
GUI 线程 Render Thread
══════════ ════════════════
1. 属性变更:
setProperty("width", 200)
→ QQuickItem::polish() 排队
2. 信号到达 Tick:
→ QQuickItem::update()
→ item 标记 dirty
─────────────────────────────────────────────────
3. Sync阶段(阻塞—挂起GUI线程)
遍历所有 dirty QuickItem:
item->updatePaintNode(oldNode, ...)
↑ 你写的代码
→ 读GUI线程属性(安全—GUI线程已暂停)
→ 写 QSGNode 数据(安全—Render Thread独占)
return newRoot;
新旧 QSGNode 树 diff:
→ 回收旧节点(触发 ~QSGNode 析构链)
→ 挂载新节点
GUI线程恢复
4. Preprocess阶段
遍历 QSGNode 树(前序):
node->preprocess()
→ 计算变换矩阵
→ 更新动态状态
← UsePreprocess flag必须设置
5. Render阶段
前序遍历 QSGNode 树:
→ 对每个 QSGGeometryNode:
① 检查 DirtyGeometry → 上传顶点缓冲到GPU
② 检查 DirtyMaterial → 调用 updateUniformData
③ 绑定着色器
④ glDrawArrays
→ 批处理排序(compare分组+合并)
→ eglSwapBuffers
关键——updatePaintNode 在 Sync 阶段:可安全读 width()/color() 等属性,但不能调 window()——window() 在 Preprocess/Render 阶段才可用。
updatePaintNode vs preprocess 分工:
| 操作 | updatePaintNode(Sync) | preprocess(Render前) |
|---|---|---|
| 读取 QQuickItem 属性 | ✅ | ⚠️ 不推荐 |
| 创建/修改 QSGNode | ✅ | ✅ |
调用 window() | ❌ | ✅ |
| 创建纹理 | ❌ | ✅ |
| 计算变换矩阵 | ⚠️ 可以 | ✅ 设计目的 |
# 11.3 自定义全流程
# 11.3.1 复用与资源
class BarChartItem : public QQuickItem {
Q_OBJECT
Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)
public:
QVector<double> values() const { return m_values; }
void setValues(const QVector<double>& v) {
m_values = v;
emit valuesChanged();
update(); // ← 标记 QQuickItem dirty → 触发 updatePaintNode
}
signals:
void valuesChanged();
protected:
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
auto* root = static_cast<QSGNode*>(oldNode);
if (!root) {
root = new QSGNode(); // 复用整个子树——不要每帧重建
for (int i = 0; i < MAX_BARS; ++i) {
auto* bar = new QSGGeometryNode();
auto* geom = new QSGGeometry(
QSGGeometry::defaultAttributes_Point2D(), 4);
geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
bar->setGeometry(geom);
bar->setFlag(QSGNode::OwnsGeometry);
auto* mat = new QSGFlatColorMaterial();
bar->setMaterial(mat);
bar->setFlag(QSGNode::OwnsMaterial);
root->appendChildNode(bar); // 预创建全部 bar 节点
}
}
// 每帧只更新顶点数据——不创建新节点
for (int i = 0; i < m_values.size() && i < MAX_BARS; ++i) {
auto* bar = static_cast<QSGGeometryNode*>(root->childAtIndex(i));
updateBarGeometry(bar->geometry(), i, m_values[i]);
bar->markDirty(QSGNode::DirtyGeometry);
}
return root;
}
private:
QVector<double> m_values;
static constexpr int MAX_BARS = 64;
};
# 11.3.2 顶点缓冲深入
四种顶点属性集:
// ① Point2D —— 仅 (x, y),适合纯顶点位置
QSGGeometry::defaultAttributes_Point2D()
// 顶点大小: 2 × float = 8 bytes
// ② ColoredPoint2D —— (x, y, r, g, b, a),适合逐顶点颜色
QSGGeometry::defaultAttributes_ColoredPoint2D()
// 顶点大小: 6 × float = 24 bytes
// ③ TexturedPoint2D —— (x, y, tx, ty),适合纹理贴图
QSGGeometry::defaultAttributes_TexturedPoint2D()
// 顶点大小: 4 × float = 16 bytes
// ④ 自定义属性集
QSGGeometry::AttributeSet custom = {
3, // 属性数量
sizeof(MyVertex), // 顶点大小
{
QSGGeometry::Attribute::createWithAttributeType(0, 2,
QSGGeometry::FloatType, QSGGeometry::PositionAttribute),
QSGGeometry::Attribute::createWithAttributeType(2, 4,
QSGGeometry::FloatType, QSGGeometry::ColorAttribute),
QSGGeometry::Attribute::createWithAttributeType(6, 2,
QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute),
}
};
自定义顶点结构:
struct Vertex3D {
float x, y, z; // 位置 (offset 0)
float r, g, b, a; // 颜色 (offset 12)
float u, v; // 纹理坐标 (offset 28)
};
// sizeof(Vertex3D) = 36 bytes
# 11.3.3 频谱柱状图
class SpectrumBars : public QQuickItem {
Q_OBJECT
static constexpr int BAR_COUNT = 64;
static constexpr float BAR_WIDTH = 8.0f;
static constexpr float BAR_GAP = 2.0f;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
auto* root = static_cast<QSGNode*>(oldNode);
// 首次——预创建全部 64 个 bar
if (!root) {
root = new QSGNode();
for (int i = 0; i < BAR_COUNT; ++i) {
auto* bar = new QSGGeometryNode();
auto* geom = new QSGGeometry(
QSGGeometry::defaultAttributes_ColoredPoint2D(), 4);
geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
bar->setGeometry(geom);
bar->setFlag(QSGNode::OwnsGeometry);
auto* mat = new QSGFlatColorMaterial();
bar->setMaterial(mat);
bar->setFlag(QSGNode::OwnsMaterial);
root->appendChildNode(bar);
}
}
// 每帧更新顶点 + 颜色
float w = width();
float h = height();
float step = (BAR_WIDTH + BAR_GAP);
for (int i = 0; i < BAR_COUNT; ++i) {
float val = (i < m_data.size()) ? m_data[i] : 0.0f;
float barH = val * h;
float left = i * step;
float right = left + BAR_WIDTH;
float hue = (float)i / BAR_COUNT * 360.0f;
QColor c = QColor::fromHsv(hue, 200, 255);
auto* bar = static_cast<QSGGeometryNode*>(root->childAtIndex(i));
auto* geom = bar->geometry();
auto* verts = geom->vertexDataAsColoredPoint2D();
// 三角形带——4 个顶点画一个矩形
verts[0].set(left, h, c.redF(), c.greenF(), c.blueF(), 1.0f);
verts[1].set(left, h - barH, c.redF(), c.greenF(), c.blueF(), 1.0f);
verts[2].set(right, h, c.redF(), c.greenF(), c.blueF(), 1.0f);
verts[3].set(right, h - barH, c.redF(), c.greenF(), c.blueF(), 1.0f);
bar->markDirty(QSGNode::DirtyGeometry);
}
return root;
}
public slots:
void pushData(const QVector<float>& data) {
m_data = data;
update();
}
private:
QVector<float> m_data;
};
SpectrumBars {
id: spectrum; anchors.fill: parent
Timer { interval: 50; repeat: true; running: true
onTriggered: { var d=[]; for(var i=0;i<64;i++) d.push(Math.random()); spectrum.pushData(d) }
}
}
案例知识融合:本案例演示了 QSGNode 复用的标准模式——①首次调用预创建全部 64 个 bar 节点(含 geometry + material);②后续帧只更新顶点数据(不 new/delete);③用 ColoredPoint2D 属性集实现逐顶点着色(避免创建 64 个不同 material);④所有顶点数据直接在 GPU 缓冲里写入——零拷贝路径。
思考题:
- 如果用 64 个
QSGFlatColorMaterial(每个不同色)代替ColoredPoint2D,Scene Graph 的批处理会发生什么? verts[0].set(left, h, r, g, b, a)里的颜色值是 0~255 还是 0.0~1.0?查阅 Qt 文档确认。
# 11.4 材质着色器
# 11.4.1 QSGMaterial 体系
class GlowMaterial : public QSGMaterial {
public:
GlowMaterial() { setFlag(Blending, true); } // 开启 alpha 混合
// ① 材质类型——用于批处理分组
QSGMaterialType* type() const override {
static QSGMaterialType t;
return &t;
}
// ② 创建对应着色器
QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override {
return new GlowShader();
}
// ③ compare——同材质才能合并批处理
int compare(const QSGMaterial* other) const override {
auto* o = static_cast<const GlowMaterial*>(other);
if (m_glowColor != o->m_glowColor) return int(m_glowColor.rgba64() - o->m_glowColor.rgba64());
if (!qFuzzyCompare(m_glowRadius, o->m_glowRadius)) return m_glowRadius > o->m_glowRadius ? 1 : -1;
return 0; // 相同
}
QColor glowColor() const { return m_glowColor; }
void setGlowColor(QColor c) { m_glowColor = c; }
float glowRadius() const { return m_glowRadius; }
void setGlowRadius(float r) { m_glowRadius = r; }
private:
QColor m_glowColor = Qt::white;
float m_glowRadius = 1.0f;
};
compare() 的核心作用——Scene Graph 在批处理排序时调用 compare() 判断两个材质是否相同:
compare(A, B) == 0 → 相同材质 → 可以合批 ← 减少 Draw Call
compare(A, B) != 0 → 不同材质 → 不能合批
# 11.4.2 着色器编写
class GlowShader : public QSGMaterialShader {
public:
// Qt 6 新版接口——两个单独的方法
bool updateUniformData(RenderState& state, QSGMaterial* newMat,
QSGMaterial* oldMat) override
{
auto* mat = static_cast<GlowMaterial*>(newMat);
// 仅在 uniform 值变化时才更新
bool changed = false;
QByteArray* buf = state.uniformData();
Q_ASSERT(buf->size() >= 36);
if (!oldMat || mat->glowColor() != static_cast<GlowMaterial*>(oldMat)->glowColor()
|| state.isMatrixDirty()) {
QMatrix4x4 mvp = state.combinedMatrix();
memcpy(buf->data(), mvp.constData(), 64); // MVP 矩阵
changed = true;
}
if (!oldMat || mat->glowColor() != static_cast<GlowMaterial*>(oldMat)->glowColor()) {
float r = mat->glowColor().redF();
float g = mat->glowColor().greenF();
float b = mat->glowColor().blueF();
memcpy(buf->data() + 64, &r, 4);
memcpy(buf->data() + 68, &g, 4);
memcpy(buf->data() + 72, &b, 4);
changed = true;
}
if (!oldMat || mat->glowRadius() != static_cast<GlowMaterial*>(oldMat)->glowRadius()) {
float r = mat->glowRadius();
memcpy(buf->data() + 76, &r, 4);
changed = true;
}
return changed;
}
// ② 加载预编译着色器(qt_add_shaders 构建系统)
void setShaderFiles(ShaderType type, QStringList& files) override {
if (type == VertexShader)
files << QStringLiteral(":/shaders/glow.vert.qsb");
else if (type == FragmentShader)
files << QStringLiteral(":/shaders/glow.frag.qsb");
}
};
GLSL 着色器源码(构建时编译为 .qsb):
// glow.vert — 顶点着色器
#version 440
layout(location = 0) in vec4 vertexCoord;
layout(std140, binding = 0) uniform buf {
mat4 matrix;
vec4 glowColor;
float glowRadius;
} ubuf;
out gl_PerVertex { vec4 gl_Position; };
out vec2 texCoord;
void main() {
gl_Position = ubuf.matrix * vertexCoord;
}
// glow.frag — 片元着色器
#version 440
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 matrix;
vec4 glowColor;
float glowRadius;
} ubuf;
void main() {
// 简单发光效果:向外衰减
vec2 uv = gl_FragCoord.xy / vec2(800.0, 480.0);
float dist = length(uv - 0.5);
float glow = exp(-dist * ubuf.glowRadius);
fragColor = ubuf.glowColor * glow;
}
# 11.4.3 uniform 批处理
uniform 更新的优化策略——利用 oldMat 参数跳过未变化的字段:
bool updateUniformData(RenderState& state, QSGMaterial* newMat,
QSGMaterial* oldMat) override
{
bool changed = false;
// 仅在 oldMat == nullptr(首次)或值变化时更新
if (!oldMat || mat->glowRadius() != static_cast<GlowMaterial*>(oldMat)->glowRadius()) {
// 更新 uniform
changed = true;
}
return changed; // false → Scene Graph 跳过本次 uniform 上传
}
# 11.4.4 发光矩形
class GlowRectangle : public QQuickItem {
Q_OBJECT
Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
Q_PROPERTY(float glowRadius READ glowRadius WRITE setGlowRadius NOTIFY glowRadiusChanged)
public:
QColor glowColor() const { return m_glowColor; }
void setGlowColor(QColor c) { m_glowColor = c; emit glowColorChanged(); update(); }
float glowRadius() const { return m_glowRadius; }
void setGlowRadius(float r) { m_glowRadius = r; emit glowRadiusChanged(); update(); }
signals:
void glowColorChanged();
void glowRadiusChanged();
protected:
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
auto* node = static_cast<QSGGeometryNode*>(oldNode);
auto* mat = node ? static_cast<GlowMaterial*>(node->material()) : nullptr;
if (!node) {
node = new QSGGeometryNode();
auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4);
geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
updateGeometry(geom); // 更新矩形顶点
node->setGeometry(geom);
node->setFlag(QSGNode::OwnsGeometry);
mat = new GlowMaterial();
node->setMaterial(mat);
node->setFlag(QSGNode::OwnsMaterial);
}
// 更新材质 uniform
mat->setGlowColor(m_glowColor);
mat->setGlowRadius(m_glowRadius);
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
private:
QColor m_glowColor = Qt::white;
float m_glowRadius = 1.0f;
};
案例知识融合:本案例把自定义材质的完整链路串起来——①QSGMaterial 子类持有 uniform 数据;②QSGMaterialShader 负责将 uniform 打包到 GPU buffer;③compare() 决定批处理能否合并同类材质;④DirtyMaterial 标记使 Scene Graph 在下一帧调用 updateUniformData。
# 11.4.5 批处理断点
写了自定义 Shader 后,最容易被忽略的是批处理断点——Scene Graph 在 Render 阶段按前序遍历收集所有 QSGGeometryNode,对相邻的同材质节点合并为一个 Draw Call。以下任何差异都会打断合并:
Scene Graph 批处理收集器:
for (node : QSGNode树(前序)):
if (node是QSGGeometryNode):
if (node.material.type() == currentBatch.material.type()
&& node.material.compare(currentBatch.material) == 0 ← 同材质?
&& node.geometry.attributes == currentBatch.attributes ← 同顶点格式?
&& node.blendState == currentBatch.blendState): ← 同混合模式?
→ 追加到当前 batch
else:
→ flush 当前 batch → glDrawArrays
→ 开启新 batch,以当前 node 为起点
6 种断点来源及避免策略:
| 断点来源 | 触发条件 | 避免策略 |
|---|---|---|
| 材质类型不同 | material.type() != prev.type() | 同类节点放一起——不要穿插不同材质 |
compare() 不相等 | uniform 值不同(如不同颜色) | 逐顶点颜色(ColoredPoint2D)代替多个 material |
| 顶点属性集不同 | Point2D vs TexturedPoint2D | 同一 batch 用同一种 attribute set |
| 混合状态不同 | 有 Blending vs 无 | 分组:不透明节点在前、半透明在后 |
| 纹理不同 | textureId != prev.textureId | Texture Atlas 合并小图 |
| 编译或变换 | QSGOpacityNode / QSGTransformNode 包裹 | 用 QSGMaterial 的 opacity 字段代替额外节点 |
探索性诊断——"我写了自定义 Shader 后 Draw Call 反而从 5 涨到 50"
现象: QML声明式 Rectangle x 50 = 5 Draw Call(QSG批处理合并)
换成自定义 GlowRectangle x 50 = 50 Draw Call(全部无法合批)
↓
排查: 检查 compare() 实现:
int compare(const QSGMaterial* other) const override {
return int(m_glowColor.rgba64() - otherColor.rgba64());
}
→ 第 1 个和第 2 个颜色相同,但第 2 个和第 3 个不同 → 连续合批被拆散
→ 修复: 逐顶点颜色 + 只读 uniform(glowRadius 同值可合并)
→ compare() 先用 glowRadius 分组,不比较逐顶点颜色
→ 50 个 Rectangle 变回 1 个 Draw Call(如果 glowRadius 全相同)
# 11.4.6 内置材质选用决策
在"够用"的前提下,优先用内置——省着色器编译、省 uniform 开销:
| 内置材质类 | 适用场景 | Draw Call 影响 |
|---|---|---|
QSGFlatColorMaterial | 纯色矩形/文字 | ✅ 极简——单 uniform(color) |
QSGVertexColorMaterial | 逐顶点颜色(渐变色/粒子) | ✅ 同批处理——无 per-material uniform |
QSGTextureMaterial | 纹理贴图 | ⚠️ 不同纹理 = 不同 batch |
QSGOpaqueTextureMaterial | 不透明纹理(无 alpha) | ✅ 比 TextureMaterial 少一次 alpha test |
# 11.5 零拷贝纹理
# 11.5.1 原生纹理
// GPU 纹理句柄已经在显存中——不需要 CPU memcpy
GLuint nativeTexId = /* 来自摄像头 DMA / EGL Image / 外部 GL 上下文 */;
QQuickWindow* win = window(); // ← 只能在 Render Thread 中获取
auto* texture = win->createTextureFromNativeObject(
QQuickWindow::NativeObjectTexture,
&nativeTexId, // ← GLuint 纹理句柄
0, // 额外的 native 选项
QSize(1920, 1080), // 纹理尺寸
QQuickWindow::TextureHasAlphaChannel // 是否有 alpha
);
底层等价操作(Scene Graph 不拷贝纹理数据):
Window::createTextureFromNativeObject(GLuint texId):
① 创建 QSGPlainTexture 包装对象
② 设置 m_textureId = texId(直接引用外部纹理)
③ 设置 m_ownsTexture = false(析构时不 glDeleteTextures)
Scene Graph 渲染时:
glBindTexture(GL_TEXTURE_2D, texId) ← 直接绑定 GPU 中的纹理
glDrawArrays(...) ← 零拷贝绘制
# 11.5.2 摄像头链路
class CameraTextureItem : public QQuickItem {
Q_OBJECT
public slots:
// 从摄像头回调线程(或任意线程)接收新帧
void onNewFrame(GLuint texId, int width, int height) {
QMutexLocker lock(&m_mutex);
m_newTexId = texId;
m_newWidth = width;
m_newHeight = height;
m_hasNewFrame = true;
lock.unlock();
update(); // ← 必须在 GUI 线程
}
protected:
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
auto* node = static_cast<QSGGeometryNode*>(oldNode);
// 检查新帧
QMutexLocker lock(&m_mutex);
bool hasFrame = m_hasNewFrame;
GLuint texId = m_newTexId;
lock.unlock();
if (!node) {
node = new QSGGeometryNode();
// 创建全屏矩形几何——用于贴纹理
auto* geom = new QSGGeometry(
QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geom);
node->setFlag(QSGNode::OwnsGeometry);
auto* mat = new QSGTextureMaterial();
node->setMaterial(mat);
node->setFlag(QSGNode::OwnsMaterial);
}
// 更新几何(全屏矩形)
auto* geom = node->geometry();
auto* verts = geom->vertexDataAsTexturedPoint2D();
float w = width(), h = height();
verts[0].set(0, 0, 0, 0);
verts[1].set(0, h, 0, 1);
verts[2].set(w, 0, 1, 0);
verts[3].set(w, h, 1, 1);
// 新帧到来——创建新的纹理包装
if (hasFrame) {
auto* mat = static_cast<QSGTextureMaterial*>(node->material());
auto* oldTex = mat->texture();
auto* newTex = window()->createTextureFromNativeObject(
QQuickWindow::NativeObjectTexture, &texId, 0,
QSize(m_newWidth, m_newHeight));
mat->setTexture(newTex);
if (oldTex) delete oldTex; // 释放旧纹理包装
m_hasNewFrame = false;
}
node->markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
return node;
}
private:
QMutex m_mutex;
GLuint m_newTexId = 0;
int m_newWidth = 0, m_newHeight = 0;
bool m_hasNewFrame = false;
};
# 11.5.3 摄像头预览
// 完整集成——V4L2 摄像头 → DMA buffer → EGL Image → QML Item
class V4L2CameraItem : public QQuickItem {
Q_OBJECT
public:
void startCapture() {
// ① 打开 V4L2 设备
m_fd = open("/dev/video0", O_RDWR);
// ... 配置 format、请求 buffer
// ② 启动捕获线程
m_running = true;
m_thread = std::thread([this]() {
while (m_running) {
// ③ 出队 buffer → DMA buffer fd
v4l2_buffer buf = {};
ioctl(m_fd, VIDIOC_DQBUF, &buf);
// ④ DMA buffer → EGL Image → GL texture
EGLint attrs[] = { EGL_DMA_BUF_PLANE0_FD_EXT, buf.m.fd, EGL_NONE };
EGLImageKHR eglImage = eglCreateImageKHR(
eglGetCurrentDisplay(), EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT, nullptr, attrs);
GLuint texId;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
// ⑤ 通知 QML——通过信号(Qt::AutoConnection 保证线程安全)
emit newFrameReady(texId, m_width, m_height);
// ⑥ 入队 buffer 回驱动
ioctl(m_fd, VIDIOC_QBUF, &buf);
}
});
}
signals:
void newFrameReady(GLuint texId, int width, int height);
private:
int m_fd = -1;
std::thread m_thread;
std::atomic<bool> m_running{false};
int m_width = 1920, m_height = 1080;
};
V4L2CameraItem {
id: camera
anchors.fill: parent
Component.onCompleted: camera.startCapture()
}
案例知识融合:本案例打通了"V4L2 DMA buffer → EGL Image → GL texture → QML Item"的完整零拷贝链路——全程 GPU 端到端,ARM Cortex-A53 上 30fps 稳定、CPU 占用 < 8%。
# 11.6 粒子系统
class ParticleSystem : public QQuickItem {
Q_OBJECT
static constexpr int MAX_PARTICLES = 5000;
struct Particle { float x, y, vx, vy, life; };
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
auto* node = static_cast<QSGGeometryNode*>(oldNode);
if (!node) {
node = new QSGGeometryNode();
auto* geom = new QSGGeometry(
QSGGeometry::defaultAttributes_ColoredPoint2D(), MAX_PARTICLES * 4);
geom->setDrawingMode(QSGGeometry::DrawTriangles);
node->setGeometry(geom);
node->setFlag(QSGNode::OwnsGeometry);
auto* mat = new QSGVertexColorMaterial(); // 内置逐顶点颜色材质
node->setMaterial(mat);
node->setFlag(QSGNode::OwnsMaterial);
}
auto* geom = node->geometry();
auto* verts = geom->vertexDataAsColoredPoint2D();
for (int i = 0; i < m_particleCount; ++i) {
auto& p = m_particles[i];
float s = 4.0f * (p.life / 2.0f);
float a = p.life / 2.0f;
// 每个粒子 = 2 个三角形(4 个顶点)
verts[i*4+0].set(p.x-s, p.y-s, 1,1,1,a);
verts[i*4+1].set(p.x+s, p.y-s, 1,1,1,a);
verts[i*4+2].set(p.x-s, p.y+s, 1,1,1,a);
verts[i*4+3].set(p.x+s, p.y+s, 1,1,1,a);
}
geom->allocate(m_particleCount * 4);
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
void tick(float dt) {
for (int i = 0; i < m_particleCount; ++i) {
m_particles[i].x += m_particles[i].vx * dt;
m_particles[i].y += m_particles[i].vy * dt;
m_particles[i].life -= dt;
}
update();
}
Particle m_particles[MAX_PARTICLES];
int m_particleCount = 0;
};
5000 个粒子 × 4 顶点 × 24 bytes = 480 KB 顶点缓冲——一次 Draw Call,60fps 稳定。
跨章知识串连——粒子系统看似只用了本章内容,实则串起了全书 5+ 章:
| 代码点 | 背后原理 | 参考章节 |
|---|---|---|
QSGGeometryNode 单节点 + 5000 粒子 | 批处理原理:同材质单节点→单 Draw Call | §2.3.3 批处理 |
defaultAttributes_ColoredPoint2D 逐顶点颜色 | 材质共享——避免每粒子一个 material 破坏批处理 | §11.4.3 uniform |
tick(dt) + update() 双阶段 | GUI 线程 tick → Render Thread updatePaintNode | §2.4 双线程 |
markDirty(DirtyGeometry) | 脏标记链路——只在数据变时上传 GPU | §11.7 脏标 |
QSGGeometry 顶点数组直写 | 零拷贝路径——CPU 直接写 GPU 缓冲映射 | §9.5.1 零拷贝 |
MAX_PARTICLES 常量池预分配 | 避免运行时 new/delete 抖动 GC | §14.4 内存优化 |
探索性思考——为什么粒子不用 QML 的 Repeater { Rectangle } 实现?
试想用 Repeater 生成 5000 个 Rectangle 会怎样:
- 对象数量爆炸:5000 个 QQuickItem + 5000 个 QSGRectangleNode → JS 堆 + C++ 堆双重压力
- 绑定压力:每个 Rectangle 的
x/y/opacity都是绑定 → 5000 × 4 = 20000 次 evaluate/帧 - Draw Call 灾难:即使同色,QSGRectangleNode 的圆角/边框状态可能打断批处理 → 100+ Draw Call
- GC 抖动:粒子生灭 → Repeater 增删 → QQuickItem 频繁 new/delete → 掉帧毛刺
结论倒推:QML 声明式适合"元素少、状态多"的场景;粒子系统是"元素多、状态少"——用 QSGNode 自定义反而更简单。这就是"什么时候不用 QML 声明式"的判断标准。
# 11.7 线程与脏标
| 操作 | 线程 | 注意事项 |
|---|---|---|
QQuickItem::update() | GUI 线程 | 后台线程通过 emit signal + Qt::AutoConnection 桥接 |
updatePaintNode() | Render Thread Sync 阶段 | 这里创建/访问 QSGNode 是安全的 |
QSGNode::markDirty() | Render Thread | 在 updatePaintNode 内调用 |
window()->createTextureFromNativeObject() | Render Thread | 必须在 updatePaintNode 内调用 |
Q_PROPERTY setter | GUI 线程 | setter 内 emit NOTIFY + update() |
// ✅ 正确:后台线程通过信号桥接到 GUI 线程
// 传感器线程
void Sensor::onDataReady(const QVector<float>& data) {
emit newData(data); // Qt::AutoConnection → GUI 线程排队
}
// GUI 线程(connect 时自动)
connect(sensor, &Sensor::newData, qmlItem, [=](auto& d) {
item->setProperty("values", QVariant::fromValue(d)); // GUI 线程安全
});
# 11.8 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | 每帧 new QSGGeometryNode() | 频繁分配释放 → 内存碎片 | 复用 oldNode,只更新顶点数据 |
| 2 | 忘记 setFlag(OwnsGeometry) | oldNode 被 Scene Graph 回收时 geometry 泄漏 | 设置 OwnsGeometry / OwnsMaterial |
| 3 | window() 在 GUI 线程调用 | window() 仅在 Render Thread 有效 | 只在 updatePaintNode 内使用 window() |
| 4 | 后台线程直接 update() | QQuickItem::update() 必须在 GUI 线程 | 信号 Qt::AutoConnection 桥接 |
| 5 | compare() 没覆盖全部字段 | 不同参数被误判为相同 → 批处理错误 | compare() 里逐字段比较 |
# 11.9 训练题
将下面的 Rectangle 动画改为自定义 QSGNode 实现
Rectangle {
width: 200; height: 200
color: "steelblue"
NumberAnimation on scale { from: 0.5; to: 1.5; duration: 1000; loops: Animation.Infinite }
}
要求用 QSGGeometryNode + QSGTransformNode 实现同样的缩放动画。
参考答案
class PulsingRect : public QQuickItem {
Q_OBJECT
QSGNode* updatePaintNode(QSGNode* old, UpdatePaintNodeData*) override {
auto* xform = static_cast<QSGTransformNode*>(old);
if (!xform) {
xform = new QSGTransformNode();
auto* geomNode = new QSGGeometryNode();
auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4);
geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geom);
// ... 设置材质、顶点
xform->appendChildNode(geomNode);
}
QMatrix4x4 m;
m.scale(scale());
xform->setMatrix(m);
return xform;
}
};
# 11.10 速查表
| 概念 | 一句话 |
|---|---|
| QSGNode | 渲染树节点基类——Transform/Opacity/Clip/Geometry |
| QSGGeometryNode | 核心绘制节点——顶点缓冲 + 材质 |
| QSGGeometry | 顶点缓冲——Point2D/ColoredPoint2D/TexturedPoint2D |
| QSGMaterial | 材质抽象——着色器 + uniform + 纹理 |
| QSGMaterialShader | 着色器封装——updateUniformData + setShaderFiles |
| compare() | 批处理合并判断——同材质 = 合批 |
| OwnsGeometry | 节点析构时释放 geometry |
| markDirty | 标记需要更新——DirtyGeometry/DirtyMaterial/DirtyMatrix |
| createTextureFromNativeObject | 外部 GL 纹理零拷贝包装 |
| QSGVertexColorMaterial | 内置逐顶点颜色材质——免写着色器 |
| QSGFlatColorMaterial | 内置纯色材质——Rectangle 的底层 |
核心哲学:
QSGNode = QML 渲染的性能天花板。
Canvas 是 JS 画完贴纹理,
QQuickItem+QSGNode 是直接写 GPU 顶点缓冲。
两者之间隔的不是代码量——是 FBO 到零拷贝的距离。