WebGPU 调试与错误处理
原作者: webgpufundamentals Contributors
原文链接: https://webgpufundamentals.org/webgpu/lessons/webgpu-debugging.html
本文基于 webgpufundamentals 的《WebGPU Debugging and Errors》进行深度解读,提炼核心调试方法与常见陷阱,并保留关键代码示例。文中术语以 WebGPU/WGSL 规范为准,避免与 WebGL/GLSL 混淆。

一、保持 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 错误分为三类,产生时机不同:
-
JavaScript 错误:例如传递了错误类型,由 JavaScript 引擎立即抛出异常。
device.queue.writeBuffer(someTexture, ...); // 错误:第一个参数应为 GPUBuffer -
内容时间线错误:在调用 API 时同步检查,例如
createTexture的size参数必须至少包含一个元素。device.createTexture({ size: [], // 错误:必须至少有一个元素 format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING, }); -
设备时间线错误:异步发生,可通过
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 个核心优化要点
-
合理利用异步错误捕获机制
通过pushErrorScope/popErrorScope和uncapturederror事件,在不阻塞主线程的前提下定位错误,同时利用getCompilationInfo获取精确的着色器错误信息,大幅提升调试效率。 -
简化与可视化是着色器调试的关键
将复杂着色器简化为纯色输出、可视化法线或纹理坐标等中间数据,可以快速隔离问题所在阶段(顶点 vs. 片元),避免盲目猜测。 -
关注命令缓冲执行时机与资源生命周期
理解 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 规范 为准。
