WebGPU 调试与错误处理

原作者: webgpufundamentals Contributors
原文链接: https://webgpufundamentals.org/webgpu/lessons/webgpu-debugging.html


本文基于 webgpufundamentals 的《WebGPU Debugging and Errors》进行深度解读,提炼核心调试方法与常见陷阱,并保留关键代码示例。文中术语以 WebGPU/WGSL 规范为准,避免与 WebGL/GLSL 混淆。

WebGPU-Inspector

一、保持 JavaScript 控制台开启以查看错误

大多数浏览器都提供 JavaScript 控制台,WebGPU 通常会在此输出错误信息。这是最基础的调试手段,但开发者常常忘记打开。

二、考虑记录未捕获的错误

可以设置事件监听来捕获未处理的 WebGPU 错误,并自行记录。例如:

const device = await adapter.requestDevice();
device.addEventListener('uncapturederror', event => alert(event.error.message));

使用 alert 可能会打断流程,但也可以将消息输出到日志、页面元素等。这种方式的优势在于,即使忘记打开控制台,也能看到错误。WebGPU 自身的错误默认发送到 JavaScript 控制台,而捕获的错误则交由开发者处理。

三、帮助 WebGPU 报告错误

WebGPU 的错误是异步报告的,这是为了保持高性能。但有时你期望的错误可能不会出现,除非主动“触发”错误处理流程。例如下面的代码创建了一个必然编译失败的着色器模块:

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();

  device.addEventListener('uncapturederror', event => {
    log(event.error.message);
  });

  device.createShaderModule({
    code: /* wgsl */ `
      this shader won't compile
    `,
  });

  log('--done--');
}

在 Chrome 129 中,这段代码可能不会立即报告错误,因为 WebGPU 不会主动处理某些错误,直到调用特定函数,例如 submit。添加 device.queue.submit([]) 后,错误才会被报告:

+  // pump WebGPU
+  device.queue.submit([]);

这种情况在实际开发中较少出现,但在构造最小复现示例或逐步调试时可能遇到。如果不想让错误同时输出到控制台,可以在事件中调用 event.preventDefault()。

四、手动捕获错误:pushErrorScope 与 popErrorScope

通过 device.pushErrorScope 和 device.popErrorScope 可以手动捕获特定范围内的错误。pushErrorScope 接受三个过滤器之一:

  • 'validation':API 使用不当导致的验证错误
  • 'out-of-memory':内存分配失败
  • 'internal':内部错误(例如着色器过于复杂)

示例:

  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();

  device.addEventListener('uncapturederror', event => {
*    log('uncaptured error:', event.error.message);
  });

+  device.pushErrorScope('validation');
  device.createShaderModule({
    code: /* wgsl */ `
      this shader won't compile
    `,
  });
+  const error = await device.popErrorScope();
+  if (error) {
+    log('captured error:', error.message);
+  }

+  device.createShaderModule({
+    code: /* wgsl */ `
+      also, this shader won't compile
+    `,
+  });

  device.queue.submit([]);

  log('--done--');

popErrorScope 返回一个 Promise,可用 await 等待,也可使用 .then() 避免阻塞:

  device.pushErrorScope('validation');
  device.createShaderModule({
    code: /* wgsl */ `
      this shader won't compile
    `,
  });
  device.popErrorScope().then(error => {
    if (error) {
      log('captured error:', error.message);
    }
  });

五、不同类型的错误

WebGPU 错误分为三类,产生时机不同:

  1. JavaScript 错误:例如传递了错误类型,由 JavaScript 引擎立即抛出异常。

    device.queue.writeBuffer(someTexture, ...); // 错误:第一个参数应为 GPUBuffer
    
  2. 内容时间线错误:在调用 API 时同步检查,例如 createTexture 的 size 参数必须至少包含一个元素。

    device.createTexture({
      size: [], // 错误:必须至少有一个元素
      format: 'rgba8unorm',
      usage: GPUTextureUsage.TEXTURE_BINDING,
    });
    
  3. 设备时间线错误:异步发生,可通过 pushErrorScope / popErrorScope 或 uncapturederror 事件捕获。着色器编译错误即属此类。

六、WGSL 着色器编译错误的详细信息

调用 getCompilationInfo() 可以获取着色器编译的详细错误信息,包括行号、列号和错误消息。下面的代码将错误信息插入到源码对应位置并输出:

  device.pushErrorScope('validation');
  const code = `
      // This function
      // calls a function
      // that does not
      // exist.

      fn foo() -> vec3f {
        return someFunction(1, 2);
      }
    `;
  const module = device.createShaderModule({ code });
  device.popErrorScope().then(async error => {
    if (error) {
      const info = await module.getCompilationInfo();
      const lines = code.split('\n');
      const msgs = [...info.messages].sort((a, b) => b.lineNum - a.lineNum);

      for (const msg of msgs) {
        lines.splice(msg.lineNum, 0,
          `${''.padEnd(msg.linePos - 1)}${''.padEnd(msg.length, '^')}`,
          msg.message,
        );
      }
      log(lines.join('\n'));
    }
  });

GPUCompilationMessage 包含以下字段:

  • message:错误消息字符串
  • type:'error'、'warning' 或 'info'
  • lineNum:错误所在行号(从 1 开始)
  • linePos:错误在行内的位置(从 1 开始)
  • offset:错误在源码字符串中的偏移(从 0 开始)
  • length:需要高亮显示的字符长度

七、调试辅助工具

WebGPU-Dev-Extension

webgpu-dev-extension 提供以下调试能力:

  • 显示错误发生的 JavaScript 堆栈:自动在 WebGPU 调用周围注入 pushErrorScope / popErrorScope,捕获错误时保留原始调用堆栈。
  • 显示命令编码器错误位置:默认情况下,GPUCommandEncoder 等编码器的错误会延迟到 encoder.finish() 时才报告,该扩展可在实际出错行抛出异常。
  • 将 WGSL 错误与源码交错显示:类似上面手写的 getCompilationInfo 处理,但自动化。
  • 检查多次更新:检测同一缓冲或纹理在一次提交中是否被更新多次。

WebGPU-Inspector

WebGPU-Inspector 可以捕获所有 WebGPU 命令,并允许检查缓冲区、纹理、调用等,是理解代码实际执行顺序的利器。

八、着色器调试技巧

简化

先裁剪到最小可运行状态,再逐步添加功能。

显示纯色

在片元着色器中返回纯色可以快速判断顶点阶段是否正确。例如,将原本复杂的光照计算替换为:

return vec4f(1, 0, 0, 1);  // solid red

如果屏幕上出现红色物体,说明顶点着色器及管线配置基本正确;否则问题在顶点阶段。

可视化中间数据

例如法线可视化:

return vec4f(vsOut.normal * 0.5 + 0.5, 1);  // normal

将法线从 [-1, 1] 映射到 [0, 1] 的颜色范围,可以直观检查法线是否正确。类似地,纹理坐标可用 fract(textureCoord) 可视化。

定位问题根源

通过逐步替换变量来源,可以缩小问题范围。例如,若怀疑 normalMatrix 有问题,可以暂时直接传递原始法线:

//vsOut.normal = uni.normalMatrix * vert.normal;
vsOut.normal = vert.normal;

如果问题消失,则定位到 normalMatrix 的计算或赋值。

九、其他常见陷阱

命令缓冲不会立即执行

queue.writeBuffer 立即执行,而 pass.draw 等只是将命令添加到命令缓冲,直到 submit 才真正执行。因此以下代码是错误的:

const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass(renderPassDesc);
pass.setPipeline(somePipeline);
pass.setBindGroup(0, someBindGroupThatUsesSomeBuffer);
device.queue.writeBuffer(someBuffer, 0, data0ForBuffer);
pass.draw(numVertices);
device.queue.writeBuffer(someBuffer, 0, data1ForBuffer); // 错误!缓冲已在命令中使用
pass.draw(numVertices);
pass.end();
device.queue.submit([encoder.finish()]);

Canvas 纹理仅在当前事件内有效

context.getCurrentTexture() 返回的纹理仅在当前事件循环内有效。如果在 await 之后使用,将会失败:

const texture = context.getCurrentTexture();
await someAsyncFunction();
encoder.beginRenderPass({
  colorTarget: { view: texture, ...}, // 错误:纹理已失效
});

应在所有异步操作之后、提交命令之前调用 getCurrentTexture。

外部纹理会在下一个事件时过期

importExternalTexture 导入的视频纹理同样只在当前事件内有效。

图片/视频需要等待加载完成

图片和视频是异步加载的,直接传入 WebGPU 可能失败:

const img = new Image();
img.src = 'someUrl';
device.write.copyExternalImageToTexture({ source: img }, ...); // 错误:图片未加载

需等待加载完成,例如使用 await img.decode()。


3 个核心优化要点

  1. 合理利用异步错误捕获机制
    通过 pushErrorScope / popErrorScope 和 uncapturederror 事件,在不阻塞主线程的前提下定位错误,同时利用 getCompilationInfo 获取精确的着色器错误信息,大幅提升调试效率。

  2. 简化与可视化是着色器调试的关键
    将复杂着色器简化为纯色输出、可视化法线或纹理坐标等中间数据,可以快速隔离问题所在阶段(顶点 vs. 片元),避免盲目猜测。

  3. 关注命令缓冲执行时机与资源生命周期
    理解 WebGPU 的异步执行模型和资源有效期(如 canvas 纹理、外部纹理、命令缓冲),避免因 writeBuffer 与 draw 的执行顺序、getCurrentTexture 的时效性等问题导致难以追踪的隐性错误。


GLSL/WebGL 专业术语检查

原文讨论的是 WebGPU 及其着色语言 WGSL,并非 WebGL 或 GLSL。在翻译和解读过程中需特别注意以下术语差异:

  • 着色器语言:WebGPU 使用 WGSL(WebGPU Shading Language),而非 GLSL。原文中所有着色器代码均为 WGSL。
  • API 名称:WebGPU 中的 GPUBuffer、GPUTexture、GPUCommandEncoder、GPURenderPassEncoder 等均不同于 WebGL 中的 WebGLBuffer、WebGLTexture 等。
  • uniform 与属性:WGSL 中仍使用 uniform 关键字表示全局变量,但语法与 GLSL 有差异(例如 @group / @binding 装饰符)。
  • 片元着色器:WGSL 使用 @fragment 而非 GLSL 的 fragment 关键字,入口函数需用 fn 定义。
  • 向量类型:WGSL 使用 vec4f、vec3f 等,与 GLSL 的 vec4、vec3 类似但带有类型后缀 f(浮点)。
  • 矩阵与法线:原文中提及的 normalMatrix 属于应用层计算的矩阵,不是 WebGPU 内置概念。

在编写或调试 WGSL 代码时,避免直接套用 GLSL 语法或习惯,应以 WGSL 规范 为准。