此示例需要 WebGPU(Chrome / Edge 113+、Safari 26+)。
从检测到设备
WebGPU 的入口是 navigator.gpu。先检测它是否存在,再请求 adapter(一块物理或软件 GPU),最后从 adapter 拿到 device:你与 GPU 之间私有的逻辑连接,所有缓冲区、纹理、管线都从它创建。
js
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();即使 navigator.gpu 存在,adapter 也可能为 null,所以两步都要检查,并给用户友好的提示。
画布与格式
canvas.getContext("webgpu") 拿到上下文后要 configure:指定 device 和像素格式。getPreferredCanvasFormat() 返回当前平台最高效的格式(bgra8unorm 或 rgba8unorm)。
WGSL 与渲染管线
着色器用 WGSL 编写。这个例子没有顶点缓冲,位置和颜色直接按内置的 vertex_index 从常量数组取:
wgsl
@vertex
fn vs_main(@builtin(vertex_index) i : u32) -> VSOut {
var P = array<vec2f, 3>(vec2f(0.0, 0.72), vec2f(-0.68, -0.56), vec2f(0.68, -0.56));
...
}渲染管线把着色器和固定功能状态(图元拓扑、输出格式等)烘焙成一个不可变对象。想换拓扑就得建新管线:创建慢、切换快,这是刻意的设计。
命令编码与提交
GPU 不是一条条立即执行命令的。你先用 command encoder 录制命令,开一个 render pass(清屏、绘制),结束后 finish() 得到命令缓冲,再 queue.submit() 交给 GPU 队列。
限制在途帧数
requestAnimationFrame 每秒触发约 60 次,但 GPU 不一定跟得上。如果每帧都无条件提交,命令会在队列里越堆越多,延迟和内存一起上涨。所以渲染循环记录“在途帧数”,用 queue.onSubmittedWorkDone() 在 GPU 完成后减一,超过 2 帧就跳过本帧:
js
if (framesInFlight >= 2) return;
device.queue.submit([encoder.finish()]);
framesInFlight++;
device.queue.onSubmittedWorkDone().then(() => framesInFlight--);后面所有 WebGPU 示例都沿用这个模式。
核心 API
navigator.gpu.requestAdapteradapter.requestDevicecontext.configurenavigator.gpu.getPreferredCanvasFormatdevice.createShaderModuledevice.createRenderPipelinedevice.createCommandEncoderencoder.beginRenderPassdevice.queue.submitdevice.queue.onSubmittedWorkDone动手试试
- 1在 GUI 里把 topology 切到 line-strip 和 point-list,观察同样三个顶点的不同画法。
- 2给数组再加三个顶点,把 pass.draw(3) 改成 draw(6),画出第二个三角形。
- 3故意在 WGSL 里写错一个字,看控制台里 getCompilationInfo 报出的行号。
还没有输出。console.log 的内容会显示在这里。
运行中