经纬志
首页 / luma.gl / 教程

调试与性能

先看结论

GPU 出错时最常见的表现是「什么都没有」:没有异常、没有日志、画面全黑。 luma.gl 提供了几组开关把这类问题暴露出来,性能优化则集中在三件事上: 少画、少传、少建。

最后验证 2026-11-03 · luma.gl 9.4 · 更新 2026-10-29 · 引用本文

打开调试

创建设备时可以要求它在出错处停下、打印着色器源码、把帧缓冲的内容预览到主画布。 这些开关只在开发环境开,线上全开会显著拖慢绘制。

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)。