调试与性能
GPU 出错时最常见的表现是「什么都没有」:没有异常、没有日志、画面全黑。 luma.gl 提供了几组开关把这类问题暴露出来,性能优化则集中在三件事上: 少画、少传、少建。
打开调试
创建设备时可以要求它在出错处停下、打印着色器源码、把帧缓冲的内容预览到主画布。 这些开关只在开发环境开,线上全开会显著拖慢绘制。
const device = await luma.luma.createDevice({
type: 'webgl',
adapters: [luma.webgl2Adapter],
createCanvasContext: {
canvas: document.getElementById('cv'),
useDevicePixels: Math.min(window.devicePixelRatio, 2),
autoResize: true
},
debug: true,
debugShaders: 'errors', // 'never' | 'errors' | 'warnings' | 'always'
debugFramebuffers: true, // 把帧缓冲缩略画到画布上
onError: function (error, context) {
console.error(error, context);
}
});
| 开关 | 作用 | 代价 |
|---|---|---|
debug | 打开实现相关的检查,出错即停 | 明显的 CPU 开销 |
debugShaders | 控制着色器源码的打印时机 | 日志量大 |
debugFramebuffers | 把离屏结果画到主画布 | 额外绘制一遍 |
onError | 接收错误与上下文 | 无 |
定位着色器问题
编译失败时,日志给的是源码行号。用
getCompilationInfoSync 拿到完整信息;
怀疑驱动改写了源码时,再看 getTranslatedSource。
const fs = device.createShader({ stage: 'fragment', source: fsSource });
if (fs.compilationStatus === 'error') {
console.log(fs.getCompilationInfoSync());
console.log(fs.getTranslatedSource());
}
const pipeline = device.createRenderPipeline({
vs: vs,
fs: fs,
topology: 'triangle-list',
bufferLayout: [{ name: 'positions', stepMode: 'vertex', format: 'float32x3' }]
});
console.log(pipeline.linkStatus); // 'pending' | 'success' | 'error'
console.log(pipeline.isPending);
给命令流打标签
渲染通道上的调试标记会出现在浏览器的图形调试工具里, 用来区分「第一趟画场景」与「第二趟合成」。
const pass = device.beginRenderPass({ framebuffer: fbo });
pass.pushDebugGroup('scene-pass');
pass.insertDebugMarker('draw-terrain');
sceneModel.draw(pass);
pass.popDebugGroup();
pass.end();
把像素读回来验证
画面全黑时,先确认 GPU 到底画出了什么。把颜色附件的内容读进缓冲区, 再异步取回,比盯着画布猜要快。
const SIZE = 256;
const px = device.createBuffer({
byteLength: SIZE * SIZE * 4,
usage: luma.Buffer.COPY_DST | luma.Buffer.COPY_SRC
});
colorTex.readBuffer({}, px);
const rgba = await px.readAsync(0, px.byteLength);
console.log('左上角像素', rgba[0], rgba[1], rgba[2], rgba[3]);
性能:先少画
| 手段 | 做法 | 收益 |
|---|---|---|
| 实例化 | instanceCount + stepMode: 'instance' | 把 N 次绘制压成 1 次 |
| 像素比封顶 | Math.min(dpr, 2) | 3 倍屏像素量减少一半以上 |
| 按需渲染 | setNeedsRedraw | 静止时 GPU 占用接近 0 |
| 共享管线 | 相同配置复用同一条 RenderPipeline | 省掉链接与状态切换 |
| 减少绑定切换 | 同一张纹理连续绘制多个模型 | 降低状态开销 |
| mipmap | 缩小时用 mipmapFilter: 'linear' | 减少远处纹理抖动与取样开销 |
实例化是最直接的一项:把每个物体的偏移、缩放、颜色放进实例属性缓冲, 绘制调用仍然只有一次。
const model = new luma.Model(device, {
vs: instancedVS,
fs: fsSource,
attributes: {
positions: quadBuffer,
instanceData: instanceBuffer
},
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x2' },
{
name: 'instanceData',
stepMode: 'instance',
byteStride: 16,
attributes: [
{ attribute: 'offsets', format: 'float32x2', byteOffset: 0 },
{ attribute: 'scales', format: 'float32', byteOffset: 8 },
{ attribute: 'alphas', format: 'float32', byteOffset: 12 }
]
}
],
vertexCount: 6,
instanceCount: 5000,
topology: 'triangle-list'
});
注意事项
每帧重建资源
在 onRender 里 createBuffer、
createTexture 是常见的性能断崖。资源在初始化阶段建好,
逐帧只调 write 与 writeData。
用 readAsync 做逐帧同步
GPU 到 CPU 的回读会造成管线停顿。只在点击拾取、导出、调试时读, 不要放在渲染循环里。
把 debug 开关带到线上
debugFramebuffers 与 debugShaders: 'always'
会持续产生额外绘制与大量日志,移动端可能直接掉到十几帧。
用构建变量区分环境。
经纬志(2026)。《调试与性能》。https://www.jingweizhi.com/lumagl/tutorial/debugging-perf.html(访问日期 2026-11-03)。