WebGPU Immediates:免缓冲区向着色器传参的新特性
原文作者:webgpufundamentals Contributors
原文链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-immediates.html
引言
本文是 WebGPU 系列教程中“如何向着色器提供数据”的一部分。在之前关于 Uniforms 和 Storage Buffers 的文章中,我们通过缓冲区将数据传递给着色器。而 Immediates(立即数) 提供了一种更轻量、更直接的方式:它不依赖绑定组,而是直接在渲染通道(render pass)内设置一小块数据。
Immediates 是 WebGPU 的一项较新特性(预计 2026 年成为核心特性,所有浏览器年底前支持)。可以通过检测 navigator.gpu.wgslLanguageFeatures.has('immediate_address_space') 来判断当前环境是否支持。
Immediates 的核心概念与限制
在 WGSL 着色器中,可以声明一个 var<immediate>,它类似于一个迷你的 uniform buffer,但具有以下特点:
| 特性 | var<immediate> | var<uniform> / var<storage, ...> |
|---|---|---|
| 数量 | 每个着色器只能有 1 个 | 可声明多个绑定 |
| 大小 | 默认最多 64 字节(可通过 maxImmediateSize 申请更多) | 受设备限制,通常很大 |
| 初始化 | 必须显式初始化,否则验证错误 | 缓冲区内容自动初始化为 0 |
| 生命周期 | 在新的 compute/render pass 开始、执行 render bundle 前后被重置为未定义 | 持续有效,直到被修改 |
你可以把它想象成一个“一次性”的小型 uniform 数据,用 passEncoder.setImmediates() 来设置。它遵循与 uniform/storage 相同的内存布局规则。
示例一:用 Immediates 绘制不同颜色三角形
我们基于基础教程中的三角形示例,修改着色器加入 var<immediate>。
WGSL 着色器
struct MyImmediates {
color: vec4f,
offset: vec2f,
};
`var<immediate>` myImmediates: MyImmediates;
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
return vec4f(pos[vertexIndex] + myImmediates.offset, 0.0, 1.0);
}
@fragment fn fs() -> @location(0) vec4f {
return myImmediates.color;
}
JavaScript 渲染循环
function render() {
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder({ label: 'our encoder' });
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setImmediates(0, new Float32Array([
1, 0, 0, 1, // color
-0.4, -0.2, // offset
]));
pass.draw(3);
pass.setImmediates(0, new Float32Array([
0, 1, 0, 1, // color
0.4, -0.2, // offset
]));
pass.draw(3);
pass.setImmediates(0, new Float32Array([
0, 0, 1, 1, // color
0.0, 0.2, // offset
]));
pass.draw(3);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
setImmediates 的完整签名如下:
passEncoder.setImmediates(
byteOffset, // 在 immediates 中的字节偏移
src, // ArrayBufferView 或 ArrayBuffer
srcOffset?, // src 中的元素偏移
size?, // 要拷贝的元素数量
);
示例二:用 Immediates 作为索引选择模型和材质
由于每次只能设置 64 字节,Immediates 最常见的用途是传递索引,指向 storage buffer 中的大块数据。假设我们有两个 storage buffer 数组:
struct PerModel {
matrix: mat4x4f,
};
struct Material {
color: vec4f,
shininess: f32,
};
@group(0) @binding(0) `var<storage, read>` models: `array<PerModel>`;
@group(0) @binding(1) `var<storage, read>` materials: `array<Material>`;
通过 Immediates 传递两个索引:
struct RenderIndices {
modelNdx: u32,
materialNdx: u32,
};
`var<immediate>` renderIndices: RenderIndices;
// 在顶点着色器中
let modelMatrix = models[renderIndices.modelNdx];
// 在片元着色器中
let material = materials[renderIndices.materialNdx];
渲染时只需:
pass.setImmediates(0, new Uint32Array([modelNdx, materialNdx]));
完整着色器示例
struct Material {
color: vec4f,
};
struct PerModel {
matrix: mat4x4f,
};
struct Globals {
viewProjection: mat4x4f,
};
struct Vertex {
@location(0) position: vec4f,
};
struct MyImmediates {
modelNdx: u32,
materialNdx: u32,
};
@group(0) @binding(0) `var<storage, read>` materials: `array<Material>`;
@group(0) @binding(1) `var<storage, read>` perModel: `array<PerModel>`;
@group(0) @binding(2) `var<uniform>` glb: Globals;
`var<immediate>` imm: MyImmediates;
@vertex fn vs(v: Vertex) -> @builtin(position) vec4f {
let model = perModel[imm.modelNdx];
return glb.viewProjection * model.matrix * v.position;
}
@fragment fn fs() -> @location(0) vec4f {
let material = materials[imm.materialNdx];
return material.color;
}
创建渲染管线与顶点缓冲区
const pipeline = device.createRenderPipeline({
label: 'our select model and material via immediates pipeline',
layout: 'auto',
vertex: {
module,
buffers: [
// position
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'},
],
},
],
},
fragment: {
module,
targets: [{ format: presentationFormat }],
},
});
创建三种几何体(三角形、正方形、圆形)的顶点缓冲区,然后上传材质数据和模型数据到 storage buffer。
创建材质与模型缓冲区
const materialData = new Float32Array([
1.0, 0.5, 0.5, 1.0, // red
0.5, 1.0, 0.5, 1.0, // green
0.5, 0.5, 1.0, 1.0, // blue
1.0, 1.0, 0.5, 1.0, // yellow
1.0, 0.5, 1.0, 1.0, // magenta
0.5, 1.0, 1.0, 1.0, // cyan
]);
const numMaterials = materialData.length / 4;
const materialBuffer = device.createBuffer({
label: 'our material buffer',
size: materialData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(materialBuffer, 0, materialData);
const models = [];
const numModels = 200;
const modelData = new Float32Array(numModels * 16);
for (let i = 0; i < numModels; ++i) {
const modelNdx = i;
const materialNdx = randInt(numMaterials);
const geometryNdx = randInt(vertices.length);
const {buffer, numVertices} = vertices[geometryNdx];
const mat = mat4.translation([(Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, 0]);
mat4.rotateZ(mat, Math.random() * Math.PI * 2, mat);
mat4.scale(mat, [Math.random() * 0.1 + 0.1, Math.random() * 0.1 + 0.1, 1], mat);
modelData.set(mat, i * 16);
models.push({
numVertices,
vertexBuffer: buffer,
immediates: new Uint32Array([modelNdx, materialNdx]),
});
}
const perModelBuffer = device.createBuffer({
label: 'our per model buffer',
size: modelData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(perModelBuffer, 0, modelData);
共享的全局 uniform buffer 存储投影矩阵:
const sharedData = new Float32Array(16);
const sharedBuffer = device.createBuffer({
label: 'our shared data buffer',
size: sharedData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
创建绑定组:
const bindGroup = device.createBindGroup({
label: 'our bind group',
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: materialBuffer },
{ binding: 1, resource: perModelBuffer },
{ binding: 2, resource: sharedBuffer },
],
});
渲染循环:
function render() {
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
const aspect = context.canvas.clientWidth / context.canvas.clientHeight;
mat4.ortho(-aspect, aspect, -1, 1, -1, 1, sharedData);
device.queue.writeBuffer(sharedBuffer, 0, sharedData);
const encoder = device.createCommandEncoder({ label: 'our encoder' });
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
for (let i = 0; i < numModels; ++i) {
const { immediates, vertexBuffer, numVertices } = models[i];
pass.setImmediates(0, immediates);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(numVertices);
}
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
上面的例子绘制了 200 个随机位置、旋转、缩放和颜色的模型,每个模型的模型矩阵和材质索引都通过 Immediates 提供给着色器。
3 个核心优化要点
-
减少小型 uniform buffer 的创建与更新开销
传统做法中,每个模型或材质可能需要一个独立的 uniform buffer,并频繁调用writeBuffer更新。使用 Immediates 可以将这些只有几个字节的数据(如索引、颜色、偏移)直接通过setImmediates设置,避免了大量小缓冲区的分配、上传和绑定开销,尤其适合每帧变化的小数据。 -
降低 API 调用次数与状态切换成本
使用 Immediates 时,无需为每个模型切换不同的绑定组(setBindGroup),只需在同一个渲染通道内多次调用setImmediates和draw。这减少了绑定组切换、着色器参数更新等状态管理操作,使渲染循环更紧凑,特别适合批量渲染大量相似对象。 -
将大数据驻留在 Storage Buffer,仅用 Immediates 传递索引
由于 Immediates 仅有 64 字节,最佳实践是将模型矩阵、材质属性等大数据放入 storage buffer 中,通过 Immediates 传递一个或几个索引来间接寻址。这种模式兼顾了灵活性与性能:共享的 uniform buffer 存储全局参数(如 viewProjection),storage buffer 存储海量对象数据,Immediates 作为轻量级的“选择器”,在绘制调用之间高效切换。
GLSL/WebGL 专业术语检查
尽管本文讨论的是 WebGPU(使用 WGSL 着色语言),但其中许多概念与 WebGL/GLSL 有对应关系。下表列出关键术语及其准确翻译和对应关系,确保技术表达无误:
| WebGPU / WGSL 术语 | 中文翻译 | 对应 WebGL / GLSL 概念 |
|---|---|---|
| Immediates | 立即数 / 立即数据 | 无直接对应,类似最小化的 uniform 数组 |
| Uniform Buffer | 统一缓冲区 | UBO (Uniform Buffer Object) |
| Storage Buffer | 存储缓冲区 | SSBO (Shader Storage Buffer Object) |
| Render Pass | 渲染通道 | 一次 draw call 序列(类似 WebGL 的绘制状态集合) |
| Compute Pass | 计算通道 | WebGL 中无直接对应,可类比 compute shader 调度 |
| Bind Group | 绑定组 | 对应 WebGL 中多个 uniform/texture 绑定点的组合 |
| Pipeline | 管线 | 类似 WebGL 的 program + 状态集合 |
| WGSL | WebGPU 着色语言 | 对应 GLSL(但语法不同) |
| Vertex Shader / Fragment Shader | 顶点着色器 / 片元着色器 | 与 GLSL 中相同概念 |
@builtin(vertex_index) | 内建输入:顶点索引 | 对应 gl_VertexID |
@builtin(position) | 内建输出:裁剪空间位置 | 对应 gl_Position |
| Memory Layout | 内存布局 | 对应 std140/std430 等布局规则 |
| Byte Offset | 字节偏移 | 无直接对应,但类似 glUniform* 的偏移参数 |
注意:WebGPU 使用 var<immediate> 而不是 attribute 或 varying。在 WGSL 中,顶点着色器与片元着色器之间的数据传递称为 inter-stage variables(阶段间变量),对应 GLSL 中的 varying / in / out。本文未涉及该部分,但理解这些术语的对应关系有助于从 WebGL 迁移到 WebGPU。
总结
Immediates 为 WebGPU 开发者提供了一种轻量、低延迟的数据传递方式,尤其适用于那些每绘制调用都可能变化的小数据。通过结合 storage buffer 和 shared uniform buffer,Immediates 可以在保持高性能的前提下大幅简化渲染代码。尽管 64 字节限制看似苛刻,但只要善于利用它传递索引或小状态,就能获得显著的性能优化。
本文基于 webgpufundamentals.org 的 “WebGPU Immediates” 教程深度编译,原作者为 webgpufundamentals Contributors。
