主题
WebGPU 概览
WebGPU 是 W3C「GPU for the Web」工作组制定的新一代 Web 图形与计算 API。它不是 WebGL 的又一个版本号,而是把 Vulkan / Metal / Direct3D 12 这一代显式图形 API 的设计原样搬到了浏览器里:由你负责描述管线、绑定资源、编码命令,由浏览器负责把同一份 JS 代码映射到各平台的原生 API。
代价是代码更长、需要自己管理资源生命周期;收益是 CPU 侧开销更低、行为更可预测、并且第一次在 Web 上带来了通用计算着色器。
WebGPU 当前属于 Limited availability
Chrome/Edge 113+、Safari 18+ 已默认启用,Chrome Android 121+ / iOS 18+ 可用;Firefox 尚未默认启用(需要在 about:config 中打开 dom.webgpu.enabled)。上生产前请先确认目标用户覆盖率,参考 caniuse.com/webgpu 与 web-features,并始终准备降级路径。
定位:从 WebGL 的「状态机」到 WebGPU 的「显式对象」
| 维度 | WebGL 1 / 2 | WebGPU |
|---|---|---|
| 底层对应 | OpenGL ES 2.0 / 3.0 | Vulkan / Metal / Direct3D 12 |
| 状态管理 | 全局状态机:bindBuffer、enable、useProgram,随时可改 | 不可变对象:管线、绑定组、缓冲区都是显式参数 |
| 绘制描述 | 调用 drawArrays 时状态由当前全局状态决定 | 绘制时必须先 setPipeline + setBindGroup + setVertexBuffer |
| 着色器语言 | GLSL ES(字符串 + 运行期编译) | WGSL(强类型、现代语法,编译日志可读) |
| 初始化 | 同步 getContext('webgl2') | 异步:requestAdapter() → requestDevice() |
| 计算着色器 | 无(只能靠绘制到纹理绕路) | 一等公民 @compute + 存储缓冲区 |
| 命令提交 | 调用即生效(隐式排队) | GPUCommandEncoder 编码 → queue.submit() 显式提交 |
| 多线程 | 主线程上下文不可跨线程 | 可在 Worker 里编码命令(OffscreenCanvas) |
| 资源释放 | deleteBuffer 等(GC 兜底) | buffer.destroy() / texture.destroy(),越早释放越好 |
三条最需要先记住的差异:
- 没有全局状态。WebGPU 把「管线状态 + 着色器 + 顶点布局」打包成一个不可变的
GPURenderPipeline,「资源 + 绑定编号」打包成GPUBindGroup,绘制时一起喂给命令编码器。想改状态就换管线(换管线不便宜,所以要尽量复用)。 - 一切都要
await。适配器、设备、编译信息、mapAsync都是 Promise;页面上要准备好「初始化中」的过渡状态。 - 校验是显式且异步的。错误不会中断 JS,而是通过
pushErrorScope/popErrorScope或uncapturederror事件报告,不主动检查就会静默什么都不画。
WGSL 简介
WGSL(WebGPU Shading Language) 是 WebGPU 唯一支持的着色语言,语法接近 Rust,强类型、无隐式转换。
wgsl
struct Uniforms {
mvp: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms; // 绑定:第 0 个绑定组的第 0 号槽位
struct VertexOut {
@builtin(position) position: vec4<f32>, // 内置输出:裁剪空间坐标
@location(0) color: vec3<f32>, // 自定义插值变量
};
@vertex
fn vsMain(@location(0) position: vec3<f32>, @location(1) color: vec3<f32>) -> VertexOut {
var out: VertexOut;
out.position = u.mvp * vec4<f32>(position, 1.0);
out.color = color;
return out;
}
@fragment
fn fsMain(in: VertexOut) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
本站约定:着色器写成模板字符串常量(形如 const shaderSource = /* wgsl */ ...),入口一律是 vsMain / fsMain / csMain,uniform 一律是 @group(0) @binding(0) 配合 var<uniform> 绑定。语法细节见 WGSL 着色语言。
本节地图
| 页面 | 内容 | 什么时候读 |
|---|---|---|
| 概览(本页) | 定位、差异、最小流程、兼容性 | 第一次接触 WebGPU |
| 适配器、设备与能力 | requestAdapter / requestDevice、limits、features、错误作用域 | 需要按设备能力做降级 |
| 缓冲区与数据传输 | usage 位标志、writeBuffer、读回、顶点/索引布局 | 上传数据、读回结果 |
| WGSL 着色语言 | 类型、绑定、内置变量与函数、编译错误排查 | 写着色器的时候 |
| 渲染管线与顶点布局 | 管线描述符逐字段、深度、混合、图元与裁剪 | 画面不对、状态不知道怎么配 |
| 纹理、采样器与 mipmap | 纹理上传、采样器、mipmap | 贴图相关 |
| 3D 渲染实战 | 深度缓冲、相机、光照、模型 | 做完整 3D 场景 |
| 计算管线与 GPGPU | 工作组、存储缓冲、并行归约 | GPU 通用计算 |
| 工程实践与兼容性 | 管线缓存、性能、跨浏览器降级 | 上生产之前 |
最小 Hello WebGPU 流程
无论做多复杂的渲染,骨架都是这七步:
js
// ① 入口:navigator.gpu(不是所有浏览器都有)
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }) // ② 选 GPU
const device = await adapter.requestDevice() // ③ 逻辑设备
const context = canvas.getContext('webgpu') // ④ 画布上下文
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'premultiplied' })
const module = device.createShaderModule({ code }) // ⑤ WGSL 模块
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex, fragment }) // ⑥ 管线
// ⑦ 每帧:编码 → 提交
const encoder = device.createCommandEncoder()
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(), loadOp: 'clear', storeOp: 'store' }] })
pass.setPipeline(pipeline)
pass.setVertexBuffer(0, vertexBuffer)
pass.draw(3)
pass.end()
device.queue.submit([encoder.finish()])1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
本站示例统一通过 shared/runtime.js 的 createGPU(container, options) 完成 ①~④,它返回 { canvas, adapter, device, context, format, configure(), dispose() },示例里只需要关心 ⑤~⑦ 和资源清理。
最小 Hello WebGPU:旋转三角形createGPU 初始化 + WGSL 管线 + uniform 每帧更新
examples/webgpu/hello-webgpu.jsjs
/**
* 【WebGPU 入门】最小渲染流程:一个三角形 + 设备能力摘要
* 演示:navigator.gpu → requestAdapter → requestDevice → context.configure → 管线 → 每帧命令编码。
*/
import { createGPU, rafLoop, cleanupAll, notSupported } from '../shared/runtime.js'
import { createUniformBuffer, writeBuffer, describeAdapter, describeLimits } from '../shared/webgpu.js'
import { mat4, degToRad, hexToVec4 } from '../shared/math.js'
import { createPanel } from '../shared/ui.js'
const shaderSource = /* wgsl */ `
struct Uniforms {
mvp: mat4x4<f32>,
tint: vec4<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VertexOut {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vsMain(
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>
) -> VertexOut {
var out: VertexOut;
out.position = u.mvp * vec4<f32>(position, 1.0);
out.color = color;
return out;
}
@fragment
fn fsMain(in: VertexOut) -> @location(0) vec4<f32> {
return vec4<f32>(in.color * u.tint.rgb, u.tint.a);
}
`
/** 在画布下方渲染一组"设备摘要"卡片(纯 DOM,与 GPU 无关)。 */
function renderSummary(container, format, adapter, limits) {
const grid = document.createElement('div')
grid.className = 'demo-grid'
const info = describeAdapter(adapter)
const l = describeLimits(limits)
const mib = (v) => (v === undefined ? '—' : `${Math.round(v / 1024 / 1024)} MiB`)
const kib = (v) => (v === undefined ? '—' : `${Math.round(v / 1024)} KiB`)
const rows = [
['canvas format', format],
['adapter', `${info.vendor} · ${info.architecture}`],
['maxTextureDimension2D', String(l.maxTextureDimension2D ?? '—')],
['maxBufferSize', mib(l.maxBufferSize)],
['maxBindGroups', String(l.maxBindGroups ?? '—')],
['maxUniformBufferBindingSize', kib(l.maxUniformBufferBindingSize)]
]
for (const [key, value] of rows) {
const card = document.createElement('div')
card.className = 'demo-card'
const name = document.createElement('div')
name.className = 'demo-mono'
name.style.color = 'var(--vp-c-text-3)'
name.textContent = key
const val = document.createElement('div')
val.className = 'demo-mono'
val.textContent = value
card.append(name, val)
grid.appendChild(card)
}
container.appendChild(grid)
}
export default async function (container) {
if (typeof navigator === 'undefined' || !navigator.gpu) {
notSupported(container, '当前浏览器不支持 WebGPU。', 'Chrome/Edge 113+、Safari 18+ 可直接运行。')
return
}
let view
try {
view = await createGPU(container, { width: 640, height: 360 })
} catch (err) {
notSupported(container, err.message, 'WebGPU 目前属 Limited availability,详见 https://caniuse.com/webgpu')
return
}
const { device, context, format, adapter } = view
// ① 顶点数据:交错存放 position(3×f32) + color(3×f32) = 每顶点 24 字节
const vertices = new Float32Array([
0, 0.65, 0, 1, 0.45, 0.4,
-0.7, -0.5, 0, 0.35, 1, 0.7,
0.7, -0.5, 0, 0.5, 0.6, 1
])
const vertexBuffer = device.createBuffer({
label: 'triangle vertices',
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
writeBuffer(device, vertexBuffer, vertices)
// ② uniform:mat4x4(64B) + vec4(16B) = 80 字节
const uniformBuffer = createUniformBuffer(device, 80, 'mvp + tint')
const uniforms = new Float32Array(20)
// ③ 着色器模块与渲染管线(layout: 'auto' 会自动生成绑定组布局)
const module = device.createShaderModule({ label: 'hello triangle', code: shaderSource })
const pipeline = device.createRenderPipeline({
label: 'hello pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vsMain',
buffers: [
{
arrayStride: 24,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32x3' }
]
}
]
},
fragment: { module, entryPoint: 'fsMain', targets: [{ format }] },
primitive: { topology: 'triangle-list' }
})
const bindGroup = device.createBindGroup({
label: 'uniforms',
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }]
})
let spin = true
let speed = 0.8
let tint = hexToVec4('#ffffff', 1)
let angle = 0
const panel = createPanel(container, { title: '参数' })
panel.checkbox({ label: '自动旋转', value: true, onChange: (v) => { spin = v } })
panel.slider({ label: '角速度', min: 0, max: 3, step: 0.1, value: 0.8, format: (v) => `${v.toFixed(1)} rad/s`, onInput: (v) => { speed = v } })
panel.color({ label: '色调', value: '#ffffff', onInput: (v) => { tint = hexToVec4(v, 1) } })
panel.note('画布由 createGPU 创建,device / format / limits 见下方卡片。')
function frame(time, delta) {
if (spin) angle += delta * speed
const aspect = view.width / view.height || 1
const projection = mat4.perspective(degToRad(45), aspect, 0.1, 100)
const camera = mat4.lookAt([0, 0, 2.4], [0, 0, 0], [0, 1, 0])
const model = mat4.fromRotationY(angle)
uniforms.set(mat4.multiplyAll(projection, camera, model), 0)
uniforms.set(tint, 16)
writeBuffer(device, uniformBuffer, uniforms)
const encoder = device.createCommandEncoder()
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.043, g: 0.071, b: 0.125, a: 1 },
loadOp: 'clear',
storeOp: 'store'
}
]
})
pass.setPipeline(pipeline)
pass.setBindGroup(0, bindGroup)
pass.setVertexBuffer(0, vertexBuffer)
pass.draw(3)
pass.end()
device.queue.submit([encoder.finish()])
}
const loop = rafLoop(frame)
// 首帧之后画布尺寸才稳定,所以摘要放在 device 就绪后立即渲染(与尺寸无关)
renderSummary(container, format, adapter, device.limits)
return cleanupAll(loop, view, panel, () => {
vertexBuffer.destroy()
uniformBuffer.destroy()
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
上面的示例同时把 device、首选画布格式和几个关键 limits 打在了画布下方——这些值直接决定你能创建多大的纹理、多大的缓冲区。
如果只想知道「这台机器到底能不能跑 WebGPU」,用第二个示例逐项自检:
环境自检面板逐步执行 requestAdapter / requestDevice 并展示 adapter.info、features 与 limits
examples/webgpu/webgpu-check.jsjs
/**
* 【WebGPU 环境自检】逐项检测浏览器是否具备可用的 WebGPU 能力
* 演示:navigator.gpu、getPreferredCanvasFormat、requestAdapter(含 forceFallbackAdapter)、
* adapter.info / features,以及 requestDevice 与 limits 清单;全部用 DOM 渲染结果。
*/
import { notSupported } from '../shared/runtime.js'
import { describeAdapter, describeLimits } from '../shared/webgpu.js'
const LIMIT_KEYS = [
'maxTextureDimension2D',
'maxTextureArrayLayers',
'maxBindGroups',
'maxVertexBuffers',
'maxVertexAttributes',
'maxBufferSize',
'maxUniformBufferBindingSize',
'maxStorageBufferBindingSize',
'maxComputeWorkgroupSizeX',
'maxComputeInvocationsPerWorkgroup'
]
/** 检测项:一行「名称 + 状态 + 说明」。 */
function createRow(parent, label) {
const row = document.createElement('div')
row.className = 'demo-card'
row.style.display = 'flex'
row.style.alignItems = 'baseline'
row.style.gap = '8px'
const name = document.createElement('span')
name.className = 'demo-mono'
name.style.minWidth = '190px'
name.textContent = label
const badge = document.createElement('span')
badge.className = 'demo-badge'
badge.textContent = '检测中…'
const detail = document.createElement('span')
detail.className = 'demo-mono'
detail.style.color = 'var(--vp-c-text-3)'
detail.style.wordBreak = 'break-all'
row.append(name, badge, detail)
parent.appendChild(row)
return {
/** @param {'ok'|'fail'|'skip'|'wait'} state */
set(state, text) {
badge.textContent = state === 'ok' ? '可用' : state === 'fail' ? '不可用' : state === 'skip' ? '跳过' : '检测中…'
badge.style.opacity = state === 'fail' ? '0.65' : '1'
detail.textContent = text || ''
}
}
}
export default async function (container) {
if (typeof navigator === 'undefined' || !navigator.gpu) {
notSupported(
container,
'当前浏览器不支持 WebGPU:navigator.gpu 不存在。',
'Chrome/Edge 113+、Safari 18+ 可直接运行;Firefox 需在 about:config 打开 dom.webgpu.enabled。'
)
return
}
const root = document.createElement('div')
root.className = 'demo-dom'
root.style.display = 'flex'
root.style.flexDirection = 'column'
root.style.gap = '8px'
container.appendChild(root)
const intro = document.createElement('p')
intro.className = 'demo-hint'
intro.style.textAlign = 'left'
intro.textContent = '逐步执行与 createGPU 内部相同的初始化流程,每一步的真实返回值都记录在下面。'
root.appendChild(intro)
const devices = []
const rGpu = createRow(root, 'navigator.gpu')
rGpu.set('ok', '存在,说明浏览器暴露了 WebGPU 入口')
const rFormat = createRow(root, 'getPreferredCanvasFormat()')
let format = '—'
try {
format = navigator.gpu.getPreferredCanvasFormat()
rFormat.set('ok', format)
} catch (err) {
rFormat.set('fail', err.message)
}
// ① requestAdapter:高性能适配器
const rAdapter = createRow(root, 'requestAdapter()')
let adapter = null
try {
const t0 = performance.now()
adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' })
const ms = (performance.now() - t0).toFixed(1)
if (adapter) rAdapter.set('ok', `${format} · 耗时 ${ms}ms`)
else rAdapter.set('fail', '返回 null:没有可用适配器(驱动被禁用 / 软件渲染被策略拦截)')
} catch (err) {
rAdapter.set('fail', err.message)
}
// ② requestAdapter:强制软件回退适配器(用于对照硬件适配器是否存在)
const rFallback = createRow(root, 'requestAdapter(forceFallback)')
try {
const fallback = await navigator.gpu.requestAdapter({ forceFallbackAdapter: true })
if (fallback) rFallback.set('ok', '存在软件回退适配器(通常是 SwiftShader / WARP)')
else rFallback.set('skip', '浏览器没有提供软件回退适配器,这不影响硬件路径')
} catch (err) {
rFallback.set('skip', err.message)
}
// ③ adapter.info:厂商与架构信息
const rInfo = createRow(root, 'adapter.info')
if (adapter) {
const info = describeAdapter(adapter)
rInfo.set('ok', `${info.vendor} / ${info.architecture} / ${info.device}${info.description && info.description !== '未知' ? ` / ${info.description}` : ''}`)
} else {
rInfo.set('skip', '没有适配器,无法读取')
}
// ④ adapter.features:可选特性集合
const rFeatures = createRow(root, 'adapter.features')
if (adapter) {
const features = [...adapter.features].sort()
rFeatures.set('ok', `共 ${features.length} 项`)
if (features.length) {
const list = document.createElement('div')
list.className = 'demo-grid'
for (const name of features) {
const tag = document.createElement('span')
tag.className = 'demo-badge demo-mono'
tag.textContent = name
list.appendChild(tag)
}
root.appendChild(list)
}
} else {
rFeatures.set('skip', '没有适配器,无法读取')
}
// ⑤ requestDevice:真正拿到设备(这一步才会校验 requiredFeatures / requiredLimits)
const rDevice = createRow(root, 'requestDevice()')
if (adapter) {
try {
const device = await adapter.requestDevice({ label: 'probe' })
devices.push(device)
rDevice.set('ok', '设备创建成功,可立即用于创建管线与缓冲区')
} catch (err) {
rDevice.set('fail', err.message)
}
} else {
rDevice.set('skip', '没有适配器,无法创建设备')
}
// ⑥ 常用 limits:按需做降级判断
const rLimits = createRow(root, 'device.limits(常用项)')
if (devices.length) {
const limits = describeLimits(devices[0].limits)
rLimits.set('ok', `列出 ${LIMIT_KEYS.length} 项常用限制`)
const grid = document.createElement('div')
grid.className = 'demo-grid'
for (const key of LIMIT_KEYS) {
const value = devices[0].limits[key]
const card = document.createElement('div')
card.className = 'demo-card'
const name = document.createElement('div')
name.className = 'demo-mono'
name.style.color = 'var(--vp-c-text-3)'
name.textContent = key
const val = document.createElement('div')
val.className = 'demo-mono'
val.textContent = value === undefined ? '未暴露' : `${value}${key === 'maxBufferSize' || key === 'maxStorageBufferBindingSize' || key === 'maxUniformBufferBindingSize' ? ` (${Math.round(value / 1024 / 1024)} MiB)` : ''}`
card.append(name, val)
grid.appendChild(card)
}
root.appendChild(grid)
// describeLimits 只返回它关心的键,这里顺带验证差集
const missing = LIMIT_KEYS.filter((k) => !(k in limits))
if (missing.length) {
const note = document.createElement('p')
note.className = 'demo-hint'
note.style.textAlign = 'left'
note.textContent = `注意:describeLimits() 未覆盖 ${missing.join('、')},需要时直接读 device.limits。`
root.appendChild(note)
}
} else {
rLimits.set('skip', '没有设备,无法读取')
}
const conclusion = document.createElement('p')
conclusion.className = 'demo-hint'
conclusion.style.textAlign = 'left'
conclusion.textContent = adapter
? '结论:本环境可以运行本站全部 WebGPU 示例。若某项 limit 偏低,请按该值下调纹理分辨率或存储缓冲大小。'
: '结论:本环境无法运行 WebGPU 示例,请升级浏览器或改用 WebGL2 示例。'
root.appendChild(conclusion)
return () => {
for (const device of devices) device.destroy()
root.remove()
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
术语速查
读 WebGPU 文档时最先卡住的往往是名词。先记住这一张表,后面的页面会反复用到:
| 术语 | 对象 | 一句话解释 |
|---|---|---|
| 适配器 | GPUAdapter | 一块可用的物理/软件 GPU 及其能力清单(只读) |
| 设备 | GPUDevice | 逻辑设备,所有资源的工厂,绑定一个队列 |
| 队列 | device.queue | 唯一的提交入口:submit() 命令缓冲、writeBuffer() 上传数据 |
| 命令编码器 | GPUCommandEncoder | 把一连串操作录制成 GPUCommandBuffer |
| 渲染通道 | GPURenderPassEncoder | 一次「设置状态 + 绘制」的录制区间,用 beginRenderPass 开启、end() 结束 |
| 计算通道 | GPUComputePassEncoder | 同上,用于 dispatchWorkgroups |
| 管线 | GPURenderPipeline / GPUComputePipeline | 不可变的「着色器 + 全部固定状态」组合 |
| 绑定组 | GPUBindGroup | 把缓冲区、纹理、采样器按 @group(n) @binding(m) 组装成一份资源包 |
| 附件 | attachment | 渲染通道的目标:颜色纹理视图、深度模板视图 |
| 命令缓冲 | GPUCommandBuffer | 编码结果,提交给队列后由 GPU 执行 |
命令编码与提交顺序
WebGPU 里「调用 API」和「GPU 执行」是两件事:
js
const encoder = device.createCommandEncoder() // ① 创建编码器(CPU 侧对象)
const pass = encoder.beginRenderPass(descriptor) // ② 开始录制一个渲染通道
pass.setPipeline(pipeline)
pass.setVertexBuffer(0, vertexBuffer)
pass.draw(3)
pass.end() // ③ 结束录制(必须调用!)
const a = encoder.finish() // ④ 产出命令缓冲
const encoder2 = device.createCommandEncoder()
const pass2 = encoder2.beginRenderPass(descriptor2)
pass2.setPipeline(pipeline2)
pass2.draw(3, 8)
pass2.end()
const b = encoder2.finish()
device.queue.submit([a, b]) // ⑤ 按数组顺序执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
要点:
beginRenderPass之后必须end(),否则finish()会校验失败。- 同一个编码器可以录多个通道,
submit()的数组顺序就是执行顺序;从不同编码器录制的命令也可以放进同一个数组。 writeBuffer是排队执行的:它按调用顺序插入队列,所以「先 writeBuffer 再 submit」是安全的。- 忘记
submit()是最常见的「代码没报错但画面全黑」原因。
什么时候不该用 WebGPU
| 场景 | 更合适的选择 |
|---|---|
| 画图表、图标、插图,数量在几百个以内 | SVG,可选中、可被读屏、可被 CSS 主题化 |
| 简单的 2D 动画、像素处理、小型游戏 | Canvas 2D,代码量少一个数量级 |
| 需要兼容老设备(如 iOS 14 及以前) | WebGL 1,覆盖率最高 |
| 只是想把已有的 WebGL 代码跑起来 | 留在 WebGL 2,重写为 WebGPU 的收益通常不抵成本 |
| 需要 GPU 通用计算、显式内存控制、多线程编码 | WebGPU(这正是它的主场) |
学习路线
本节内容互相依赖,建议按下面的顺序读:
- 本页:先能跑起来一个三角形,建立「管线 + 命令编码」的心智模型。
- 适配器、设备与能力:理解初始化与能力查询,知道怎么做降级。
- 缓冲区与数据传输:数据怎么进 GPU、怎么读回来——这是九成 bug 的来源。
- WGSL 着色语言:写得出着色器,才谈得上调试画面。
- 渲染管线与顶点布局:把状态配置和「画面为什么不对」对应起来。
- 之后的纹理、3D 实战、计算管线与工程实践页。
前置知识
不需要先精通 WebGL,但建议知道:顶点/片元着色器各自负责什么、裁剪空间与 NDC、透视投影矩阵、深度测试、纹理 UV。这些概念在 WebGL 教程 里都有铺垫,遇到时回看即可。
API 速查
| API | 参数 | 说明 |
|---|---|---|
navigator.gpu | — | WebGPU 入口;不存在即完全不支持 |
navigator.gpu.requestAdapter(options) | { powerPreference, forceFallbackAdapter } | 异步获取 GPUAdapter,可能返回 null |
navigator.gpu.getPreferredCanvasFormat() | — | 返回 bgra8unorm 或 rgba8unorm,用于 context.configure |
adapter.requestDevice(descriptor) | { label, requiredFeatures, requiredLimits } | 异步获取 GPUDevice,失败即抛异常 |
adapter.info | — | { vendor, architecture, device, description } |
adapter.features | — | Set<string>,创建可选资源前必须显式申请 |
device.limits | — | 只读对象,所有上限值;降级时按它取 Math.min |
canvas.getContext('webgpu') | — | 返回 GPUCanvasContext |
context.configure(config) | { device, format, alphaMode, usage } | 绑定设备与格式;尺寸变化后需重新配置 |
context.getCurrentTexture() | — | 每帧取当前后备纹理,必须建视图后作为颜色附件 |
device.createCommandEncoder() | — | 命令编码器,可批量编码后一次提交 |
device.queue.submit([...]) | 命令缓冲区数组 | 提交到 GPU 队列,调用顺序即执行顺序 |
device.destroy() | — | 销毁设备,释放全部资源 |
常见坑与排查
画面全黑、控制台却没有任何报错
WebGPU 的校验错误默认是异步上报的,不会 throw。现象 → 原因 → 解决:
- 什么都没画 → 命令根本没提交:确认
pass.end()之后有device.queue.submit([encoder.finish()])。 - 只有清屏色 → 管线状态与数据不匹配:顶点
arrayStride/offset/format与 WGSL 的@location对不上,或draw()的顶点数小于 3。 getCurrentTexture()只在提交后才显示:忘记每帧调用,或 resize 后没有重新configure()。
排查手段:用 device.pushErrorScope('validation') 包住可疑调用,await device.popErrorScope() 会返回一个 GPUValidationError(详见 适配器、设备与能力)。
requestAdapter() 返回 null
常见原因是浏览器禁用了硬件加速、跑在隐私模式,或系统没有可用的 D3D12 / Vulkan 驱动。此时不要直接白屏:提示用户开启硬件加速,或降级到 WebGL2(本站在 六大图形技术选型 里给出了特性检测代码)。
一定要销毁资源
createGPU 返回的 view.dispose() 会调用 device.destroy(),一次性释放该设备的所有资源。自己创建的 GPUBuffer / GPUTexture 若长期持有,应在清理函数里显式 destroy()——示例中 return cleanupAll(loop, view, () => buffer.destroy()) 就是这个模式。
模块顶层不要碰 DOM
本站文档是 SSR 构建的,示例模块顶层访问 window / document 会让构建失败。所有 DOM 操作(包括 createGPU)都必须写在 export default async function (container) {...} 内部。