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 个核心优化要点

  1. 减少小型 uniform buffer 的创建与更新开销
    传统做法中,每个模型或材质可能需要一个独立的 uniform buffer,并频繁调用 writeBuffer 更新。使用 Immediates 可以将这些只有几个字节的数据(如索引、颜色、偏移)直接通过 setImmediates 设置,避免了大量小缓冲区的分配、上传和绑定开销,尤其适合每帧变化的小数据。

  2. 降低 API 调用次数与状态切换成本
    使用 Immediates 时,无需为每个模型切换不同的绑定组(setBindGroup),只需在同一个渲染通道内多次调用 setImmediates 和 draw。这减少了绑定组切换、着色器参数更新等状态管理操作,使渲染循环更紧凑,特别适合批量渲染大量相似对象。

  3. 将大数据驻留在 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 + 状态集合
WGSLWebGPU 着色语言对应 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。