JS `WebGPU` `Binding Groups` 与 `Layouts`:资源绑定优化

好家伙,这要求,够直接! 行,没问题! 咱们这就开始这场关于 WebGPU Binding Groups 和 Layouts 的脱口秀… 哦不,技术讲座!

开场白:各位观众,晚上好!

欢迎大家来到“GPU 的小秘密:Binding Groups 和 Layouts 的那些事儿”专场。今天咱们不聊八卦,就聊聊 WebGPU 里面那些让 GPU 高效工作的幕后英雄。如果你觉得 GPU 只是个跑游戏的,那今天这场讲座之后,你会发现它还是个资源管理大师。

第一幕:Binding Groups 和 Layouts 是什么鬼?

想象一下,你在厨房做饭,各种食材(数据)、锅碗瓢盆(资源)都需要摆放好,才能快速找到并使用。Binding Groups 和 Layouts 在 WebGPU 里就扮演着类似的角色。

  • Layouts (GPUBindGroupLayout): 相当于厨房的设计图纸,规定了食材、锅碗瓢盆的摆放位置、类型和用途。它定义了 Binding Group 应该包含哪些资源,以及这些资源如何被 Shader 使用。你可以把它理解为 “资源布局的蓝图”
  • Binding Groups (GPUBindGroup): 相当于根据厨房设计图纸摆放好的食材和锅碗瓢盆。它包含了实际的资源(buffers, textures, samplers),并且这些资源的位置和类型必须与对应的 BindGroupLayout 匹配。你可以把它理解为 “资源的实际摆放”

简单来说:Layouts 定义了 什么 资源需要绑定,Binding Groups 定义了 如何 将这些资源绑定到 Shader。

第二幕:为什么我们需要它们?

如果没有 Binding Groups 和 Layouts,那 GPU 就像一个杂乱无章的厨房,每次使用资源都得大海捞针,效率低下。有了它们,GPU 就能:

  1. 高效地访问资源: GPU 可以根据 Layout 的信息,直接找到需要的资源,避免不必要的搜索。
  2. 减少状态切换: 通过预先定义的 Layout,GPU 可以减少绘制调用之间的状态切换,提高渲染效率。
  3. 提供类型安全: Layout 确保了 Shader 使用的资源类型与实际提供的资源类型一致,避免运行时错误。

第三幕:实战演练:一个简单的 Shader 例子

咱们先来一个简单的 Shader,它接收一个 uniform buffer,并将其值乘以顶点坐标。

// vertex.wgsl (顶点着色器)
struct VertexInput {
  @location(0) position: vec3f,
};

struct VertexOutput {
  @builtin(position) position: vec4f,
};

struct Uniforms {
  multiplier: f32,
};

@group(0) @binding(0) var<uniform> uniforms: Uniforms;

@vertex
fn main(input: VertexInput) -> VertexOutput {
  var output: VertexOutput;
  output.position = vec4f(input.position * uniforms.multiplier, 1.0);
  return output;
}

// fragment.wgsl (片元着色器)
@fragment
fn main() -> @location(0) vec4f {
  return vec4f(1.0, 0.0, 0.0, 1.0); // 红色
}

这个 Shader 需要一个 uniform buffer,里面包含一个浮点数 multiplier。现在咱们来看看如何使用 Binding Groups 和 Layouts 将这个 uniform buffer 传递给 Shader。

第四幕:创建 BindGroupLayout (厨房设计图纸)

首先,我们需要创建一个 BindGroupLayout,描述 uniform buffer 的布局。

async function createBindGroupLayout(device) {
  const bindGroupLayout = device.createBindGroupLayout({
    entries: [
      {
        binding: 0, // 对应 Shader 中的 @binding(0)
        visibility: GPUShaderStage.VERTEX, // 该资源只在顶点着色器中使用
        buffer: {
          type: 'uniform', // 这是一个 uniform buffer
        },
      },
    ],
  });
  return bindGroupLayout;
}
  • binding: 0: 指定该资源在 Binding Group 中的索引,与 Shader 中的 @binding(0) 对应。
  • visibility: GPUShaderStage.VERTEX: 指定该资源在哪个 Shader Stage 中可见。这里是顶点着色器。
  • buffer: { type: 'uniform' }: 指定资源类型为 uniform buffer。

第五幕:创建 BindGroup (摆放食材)

接下来,我们需要创建一个 BindGroup,将实际的 uniform buffer 绑定到 BindGroupLayout。

async function createBindGroup(device, bindGroupLayout, uniformBuffer) {
  const bindGroup = device.createBindGroup({
    layout: bindGroupLayout, // 使用之前创建的 BindGroupLayout
    entries: [
      {
        binding: 0, // 对应 Shader 中的 @binding(0)
        resource: {
          buffer: uniformBuffer, // 实际的 uniform buffer
        },
      },
    ],
  });
  return bindGroup;
}
  • layout: bindGroupLayout: 指定该 BindGroup 使用的 BindGroupLayout。
  • resource: { buffer: uniformBuffer }: 将实际的 uniform buffer 绑定到 Binding Group。

第六幕:创建 Uniform Buffer (准备食材)

在创建 BindGroup 之前,我们需要先创建一个 uniform buffer,并写入数据。

async function createUniformBuffer(device, multiplier) {
  const uniformBufferSize = 4; // 浮点数大小为 4 字节
  const uniformBuffer = device.createBuffer({
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });

  // 将数据写入 uniform buffer
  device.queue.writeBuffer(
    uniformBuffer,
    0, // offset
    new Float32Array([multiplier]) // 数据
  );

  return uniformBuffer;
}

第七幕:创建 Pipeline (烹饪过程)

最后,我们需要创建一个 Render Pipeline,将 BindGroupLayout 应用到 Pipeline Layout 中。

async function createRenderPipeline(device, bindGroupLayout) {
  const vertexShaderModule = device.createShaderModule({
    code: `
      struct VertexInput {
        @location(0) position: vec3f,
      };

      struct VertexOutput {
        @builtin(position) position: vec4f,
      };

      struct Uniforms {
        multiplier: f32,
      };

      @group(0) @binding(0) var<uniform> uniforms: Uniforms;

      @vertex
      fn main(input: VertexInput) -> VertexOutput {
        var output: VertexOutput;
        output.position = vec4f(input.position * uniforms.multiplier, 1.0);
        return output;
      }
    `,
  });

  const fragmentShaderModule = device.createShaderModule({
    code: `
      @fragment
      fn main() -> @location(0) vec4f {
        return vec4f(1.0, 0.0, 0.0, 1.0); // 红色
      }
    `,
  });

  const renderPipeline = device.createRenderPipeline({
    layout: device.createPipelineLayout({
      bindGroupLayouts: [bindGroupLayout], // 将 BindGroupLayout 应用到 Pipeline Layout
    }),
    vertex: {
      module: vertexShaderModule,
      entryPoint: 'main',
      buffers: [
        {
          arrayStride: 12, // vec3f 的大小
          attributes: [
            {
              shaderLocation: 0, // 对应 Shader 中的 @location(0)
              offset: 0,
              format: 'float32x3',
            },
          ],
        },
      ],
    },
    fragment: {
      module: fragmentShaderModule,
      entryPoint: 'main',
      targets: [
        {
          format: navigator.gpu.getPreferredCanvasFormat(),
        },
      ],
    },
    primitive: {
      topology: 'triangle-list',
    },
  });
  return renderPipeline;
}
  • layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }): 创建 Pipeline Layout,并将之前创建的 BindGroupLayout 添加到其中。

第八幕:绘制 (开始烹饪)

最后,我们就可以使用 Render Pipeline 和 BindGroup 进行绘制了。

async function render(device, renderPipeline, bindGroup, vertexBuffer, indexBuffer, context) {
  const commandEncoder = device.createCommandEncoder();
  const textureView = context.getCurrentTexture().createView();

  const renderPassDescriptor = {
    colorAttachments: [
      {
        view: textureView,
        clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // 黑色
        loadOp: 'clear',
        storeOp: 'store',
      },
    ],
  };

  const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
  passEncoder.setPipeline(renderPipeline);
  passEncoder.setVertexBuffer(0, vertexBuffer);
  passEncoder.setIndexBuffer(indexBuffer, 'uint16');
  passEncoder.setBindGroup(0, bindGroup); // 设置 BindGroup
  passEncoder.drawIndexed(3); // 绘制一个三角形
  passEncoder.end();

  device.queue.submit([commandEncoder.finish()]);
}
  • passEncoder.setBindGroup(0, bindGroup): 将 BindGroup 设置到 Render Pass 中。 0 对应于 shader 中的 @group(0)

第九幕:完整代码示例

async function main() {
  const canvas = document.getElementById('gpu-canvas');
  const adapter = await navigator.gpu.requestAdapter();
  const device = await adapter.requestDevice();
  const context = canvas.getContext('webgpu');
  context.configure({
    device: device,
    format: navigator.gpu.getPreferredCanvasFormat(),
  });

  // 顶点数据 (一个三角形)
  const vertices = new Float32Array([
    0.0, 0.5, 0.0,
    -0.5, -0.5, 0.0,
    0.5, -0.5, 0.0,
  ]);

  // 索引数据
  const indices = new Uint16Array([0, 1, 2]);

  // 创建顶点 Buffer
  const vertexBuffer = device.createBuffer({
    size: vertices.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true,
  });
  new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
  vertexBuffer.unmap();

  // 创建索引 Buffer
  const indexBuffer = device.createBuffer({
    size: indices.byteLength,
    usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true,
  });
  new Uint16Array(indexBuffer.getMappedRange()).set(indices);
  indexBuffer.unmap();

  // 创建 BindGroupLayout
  const bindGroupLayout = await createBindGroupLayout(device);

  // 创建 Uniform Buffer
  const uniformBuffer = await createUniformBuffer(device, 2.0); // multiplier = 2.0

  // 创建 BindGroup
  const bindGroup = await createBindGroup(device, bindGroupLayout, uniformBuffer);

  // 创建 Render Pipeline
  const renderPipeline = await createRenderPipeline(device, bindGroupLayout);

  // 渲染
  render(device, renderPipeline, bindGroup, vertexBuffer, indexBuffer, context);
}

// ... (createBindGroupLayout, createBindGroup, createUniformBuffer, createRenderPipeline, render 函数的定义,参考前面的代码)

main();

第十幕:Binding Group 的类型

Binding Group 除了我们上面使用的 uniform 类型的 buffer 之外,还有其他的类型。 它们是根据 GPUBindingType 枚举定义的, 主要类型如下:

Binding Type 描述 适用 Shader Stage 示例
uniform 用于存储 uniform 变量的 buffer。 Vertex, Fragment, Compute 全局配置参数,例如光照方向,颜色等。
storage 用于存储可读写的 buffer。 Vertex, Fragment, Compute 存储顶点数据,粒子数据等。
read-only-storage 用于存储只读的 buffer。 Vertex, Fragment, Compute 存储只读的纹理数据,模型数据等。
sampler 用于纹理采样的采样器。 Vertex, Fragment 用于控制纹理过滤,寻址模式等。
sampled-texture 用于采样的纹理。 Vertex, Fragment 2D纹理,3D纹理,立方体纹理等。
storage-texture 用于读写的纹理。 Fragment, Compute 用于渲染目标,计算结果存储等。

第十一幕: Binding Group 兼容性

在使用 Binding Group 时,需要注意一些兼容性问题:

  • BindGroupLayout 兼容性: 不同的 Render Pipeline 可以使用不同的 BindGroupLayout,只要它们在逻辑上是兼容的。例如,两个 BindGroupLayout 包含相同的 binding 和类型,但顺序不同,它们仍然是兼容的。
  • BindGroup 兼容性: 一个 BindGroup 只能与一个 BindGroupLayout 关联。如果需要使用不同的资源,需要创建新的 BindGroup。

第十二幕:最佳实践

  1. 减少 Binding Group 切换: Binding Group 切换会引入性能开销。尽量将相关的资源放在同一个 Binding Group 中,减少切换次数。
  2. 使用 Pipeline Cache: Pipeline 的创建是一个耗时的过程。使用 Pipeline Cache 可以缓存 Pipeline 对象,避免重复创建。
  3. 合理使用 Binding Group 的类型: 根据资源的读写需求,选择合适的 Binding Group 类型。例如,如果资源只需要读取,可以使用 read-only-storage 类型,提高性能。

尾声:GPU 的资源管理之道

Binding Groups 和 Layouts 是 WebGPU 中非常重要的概念,它们直接影响着 GPU 的性能。理解并合理使用它们,可以帮助你写出更高效的 WebGPU 应用。希望今天的讲座能让你对 GPU 的资源管理有更深入的了解。

谢谢大家! 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注