JavaScript内核与高级编程之:`JavaScript`的`WebGPU`:如何利用 `JavaScript` `WebGPU` 在浏览器中进行 GPU 计算。

各位观众老爷,晚上好!今儿咱们来聊聊 JavaScript 的新玩具——WebGPU,这玩意儿能让咱的浏览器摇身一变,变成一个 GPU 计算平台。是不是听起来有点儿科幻?别慌,其实也没那么难,今天就带大家伙儿一块儿上手玩玩。

开场白:为啥要用 WebGPU?

话说 JavaScript 这门语言,最初的定位只是在浏览器里搞点儿小动画、验证表单啥的。但随着互联网应用越来越复杂,光靠 CPU 吭哧吭哧地算,那速度简直是蜗牛爬树。这时候,我们就想,能不能让浏览器也能用上 GPU 的强大计算能力呢?

于是乎,WebGPU 就应运而生了。它提供了一个低级的、跨平台的 API,让 JavaScript 能够直接访问 GPU 的硬件加速功能。这意味着啥?这意味着咱们可以用 JavaScript 来做一些以前想都不敢想的事情,比如:

  • 高性能的图形渲染: 复杂的 3D 场景、实时光照效果,统统不在话下。
  • 并行计算: 图像处理、物理模拟、机器学习,GPU 的并行能力简直是神器。
  • 通用计算: 只要是能并行化的任务,都可以交给 GPU 去算,让 CPU 歇歇脚。

第一幕:准备工作——硬件和软件

要玩 WebGPU,首先得确认你的设备支持。

  • 硬件要求: 必须得有一块像样的 GPU,现在的独立显卡基本都支持,集成显卡也行,但性能可能会打折扣。
  • 软件要求:
    • 浏览器: Chrome、Firefox、Safari 的最新版本都支持 WebGPU。
    • 操作系统: Windows、macOS、Linux 都可以。

确认硬件软件没问题后,就可以撸起袖子开干了!

第二幕:WebGPU 的基本概念

在深入代码之前,咱们先了解一下 WebGPU 的几个核心概念:

概念 解释
GPUAdapter 代表一个 GPU 设备。你可以把它想象成你的显卡。
GPUDevice 代表与 GPU 的一个逻辑连接。通过它,你可以创建各种资源,比如纹理、缓冲区、着色器等等。
GPUQueue 用于提交命令的队列。所有的 GPU 操作都是通过命令提交到队列中执行的。
GPUBuffer 代表 GPU 内存中的一块缓冲区。可以用来存储顶点数据、索引数据、uniform 数据等等。
GPUTexture 代表 GPU 内存中的一张纹理。可以用来存储图像数据、颜色数据等等。
GPUShaderModule 包含着色器代码的模块。着色器是用 WGSL (WebGPU Shading Language) 编写的,用于控制 GPU 如何渲染图形或执行计算。
GPURenderPipeline 定义了如何渲染图形。它包含了顶点着色器、片元着色器、以及渲染状态的配置。
GPUComputePipeline 定义了如何执行通用计算。它包含一个计算着色器,以及计算状态的配置。
GPUBindGroup 用于将资源(比如缓冲区、纹理)绑定到着色器。它可以让你在着色器中访问这些资源。
GPUCommandEncoder 用于创建命令。你可以使用它来创建渲染命令、计算命令、以及复制命令。
GPUCommandBuffer 包含一系列命令的缓冲区。你可以将它提交到 GPUQueue 中执行。

是不是有点儿晕?没关系,咱们结合代码来理解。

第三幕:第一个 WebGPU 程序—— Hello Compute!

咱们先来写一个最简单的 WebGPU 程序,实现一个向量加法的计算。

async function main() {
  // 1. 获取 GPUAdapter
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    console.error("No WebGPU adapter found.");
    return;
  }

  // 2. 获取 GPUDevice
  const device = await adapter.requestDevice();

  // 3. 创建两个输入缓冲区
  const bufferA = device.createBuffer({
    size: 16, // 4个float,每个float 4个字节
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true, // 创建时映射
  });
  new Float32Array(bufferA.getMappedRange()).set([1, 2, 3, 4]);
  bufferA.unmap(); // 解除映射

  const bufferB = device.createBuffer({
    size: 16,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true,
  });
  new Float32Array(bufferB.getMappedRange()).set([5, 6, 7, 8]);
  bufferB.unmap();

  // 4. 创建一个输出缓冲区
  const bufferResult = device.createBuffer({
    size: 16,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, // 可以作为存储,也可以复制到别的地方
  });

  // 5. 创建一个计算着色器模块
  const shaderModule = device.createShaderModule({
    code: `
      @group(0) @binding(0) var<storage, read_write> a: array<f32>;
      @group(0) @binding(1) var<storage, read_write> b: array<f32>;
      @group(0) @binding(2) var<storage, read_write> result: array<f32>;

      @compute @workgroup_size(1)
      fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
        let i: u32 = global_id.x;
        result[i] = a[i] + b[i];
      }
    `,
  });

  // 6. 创建一个计算管线
  const computePipeline = device.createComputePipeline({
    layout: 'auto', // 让 WebGPU 自动推断 layout
    compute: {
      module: shaderModule,
      entryPoint: "main",
    },
  });

  // 7. 创建一个绑定组
  const bindGroup = device.createBindGroup({
    layout: computePipeline.getBindGroupLayout(0), // 获取 layout
    entries: [
      { binding: 0, resource: { buffer: bufferA } },
      { binding: 1, resource: { buffer: bufferB } },
      { binding: 2, resource: { buffer: bufferResult } },
    ],
  });

  // 8. 创建一个命令编码器
  const commandEncoder = device.createCommandEncoder();

  // 9. 创建一个计算通道编码器
  const passEncoder = commandEncoder.beginComputePass();

  // 10. 设置管线和绑定组
  passEncoder.setPipeline(computePipeline);
  passEncoder.setBindGroup(0, bindGroup);

  // 11. 调度计算着色器
  passEncoder.dispatchWorkgroups(4); // 处理 4 个元素

  // 12. 结束计算通道
  passEncoder.end();

  // 13. 将结果从 GPU 复制到 CPU
  const readBuffer = device.createBuffer({
    size: 16,
    usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
  });
  commandEncoder.copyBufferToBuffer(bufferResult, 0, readBuffer, 0, 16);

  // 14. 提交命令
  const commandBuffer = commandEncoder.finish();
  device.queue.submit([commandBuffer]);

  // 15. 读取结果
  await readBuffer.mapAsync(GPUMapMode.READ);
  const resultArray = new Float32Array(readBuffer.getMappedRange());
  console.log("Result:", resultArray); // 输出 [6, 8, 10, 12]
  readBuffer.unmap();
}

main();

这段代码做了啥?

  1. 获取 GPU 设备: 首先,我们通过 navigator.gpu.requestAdapter()adapter.requestDevice() 获取 GPUAdapter 和 GPUDevice。
  2. 创建缓冲区: 创建了三个缓冲区:bufferAbufferB 用于存储输入数据,bufferResult 用于存储计算结果。
  3. 创建着色器模块: 创建了一个计算着色器模块 shaderModule,其中的 WGSL 代码实现了向量加法。
  4. 创建计算管线: 创建了一个计算管线 computePipeline,它定义了如何执行计算着色器。
  5. 创建绑定组: 创建了一个绑定组 bindGroup,它将缓冲区绑定到着色器。
  6. 创建命令编码器: 创建了一个命令编码器 commandEncoder,用于创建命令。
  7. 创建计算通道编码器: 创建了一个计算通道编码器 passEncoder,用于配置计算通道。
  8. 调度计算着色器: 使用 passEncoder.dispatchWorkgroups(4) 调度计算着色器,让 GPU 执行计算。
  9. 复制结果: 将计算结果从 bufferResult 复制到 readBuffer,然后读取 readBuffer 中的数据。

第四幕:深入 WGSL——WebGPU 的着色器语言

上面的例子中,我们用到了 WGSL (WebGPU Shading Language) 来编写着色器代码。WGSL 是一种专门为 WebGPU 设计的着色器语言,它借鉴了 GLSL 和 SPIR-V 的一些特性,但更加现代化、安全和易于使用。

咱们来仔细看看上面的 WGSL 代码:

@group(0) @binding(0) var<storage, read_write> a: array<f32>;
@group(0) @binding(1) var<storage, read_write> b: array<f32>;
@group(0) @binding(2) var<storage, read_write> result: array<f32>;

@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
  let i: u32 = global_id.x;
  result[i] = a[i] + b[i];
}
  • @group(0) @binding(0) var<storage, read_write> a: array<f32>; 这行代码声明了一个存储缓冲区 a,它是一个浮点数数组。@group(0)@binding(0) 指定了它在绑定组中的位置。var<storage, read_write> 表示这是一个可读写的存储缓冲区。
  • @compute @workgroup_size(1) 这行代码声明了一个计算着色器函数 main@compute 表示这是一个计算着色器,@workgroup_size(1) 表示每个工作组包含一个工作项。
  • @builtin(global_invocation_id) global_id: vec3<u32> 这行代码声明了一个内置变量 global_id,它是一个三维向量,表示当前工作项的全局 ID。
  • let i: u32 = global_id.x; 这行代码将全局 ID 的 x 分量赋值给变量 i,用于索引数组。
  • result[i] = a[i] + b[i]; 这行代码实现了向量加法,将 a[i]b[i] 相加,然后赋值给 result[i]

WGSL 的语法比较简单,但功能非常强大。你可以用它来编写各种复杂的着色器代码,实现各种炫酷的效果。

第五幕:渲染图形—— Hello Triangle!

除了通用计算,WebGPU 还可以用于图形渲染。咱们来写一个简单的程序,渲染一个三角形。

async function main() {
  // 1. 获取 GPUAdapter 和 GPUDevice
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    console.error("No WebGPU adapter found.");
    return;
  }
  const device = await adapter.requestDevice();

  // 2. 获取 canvas
  const canvas = document.getElementById("canvas");
  const context = canvas.getContext("webgpu");

  // 3. 配置 canvas
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
  context.configure({
    device: device,
    format: presentationFormat,
  });

  // 4. 创建顶点缓冲区
  const vertices = new Float32Array([
    0.0,  0.5, 0.0, // x, y, z
    -0.5, -0.5, 0.0,
    0.5, -0.5, 0.0,
  ]);
  const vertexBuffer = device.createBuffer({
    size: vertices.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true,
  });
  new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
  vertexBuffer.unmap();

  // 5. 创建着色器模块
  const shaderModule = device.createShaderModule({
    code: `
      @vertex
      fn vertexMain(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
        return vec4<f32>(pos, 1.0);
      }

      @fragment
      fn fragmentMain() -> @location(0) vec4<f32> {
        return vec4<f32>(1.0, 0.0, 0.0, 1.0); // 红色
      }
    `,
  });

  // 6. 创建渲染管线
  const renderPipeline = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: shaderModule,
      entryPoint: "vertexMain",
      buffers: [{
        arrayStride: 12, // 每个顶点 12 个字节 (3 个 float)
        attributes: [{
          shaderLocation: 0, // 对应着色器中的 @location(0)
          offset: 0,
          format: GPUVertexFormat.float32x3,
        }],
      }],
    },
    fragment: {
      module: shaderModule,
      entryPoint: "fragmentMain",
      targets: [{
        format: presentationFormat,
      }],
    },
    primitive: {
      topology: "triangle-list", // 使用三角形列表
    },
  });

  // 7. 渲染循环
  function render() {
    // 8. 获取纹理视图
    const textureView = context.getCurrentTexture().createView();

    // 9. 创建命令编码器
    const commandEncoder = device.createCommandEncoder();

    // 10. 创建渲染通道编码器
    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);

    // 11. 设置管线和顶点缓冲区
    passEncoder.setPipeline(renderPipeline);
    passEncoder.setVertexBuffer(0, vertexBuffer);

    // 12. 绘制三角形
    passEncoder.draw(3, 1, 0, 0); // 3 个顶点, 1 个实例

    // 13. 结束渲染通道
    passEncoder.end();

    // 14. 提交命令
    const commandBuffer = commandEncoder.finish();
    device.queue.submit([commandBuffer]);

    requestAnimationFrame(render); // 循环渲染
  }

  render();
}

main();

这段代码做了啥?

  1. 获取 GPU 设备和 Canvas: 首先,我们获取 GPUAdapter、GPUDevice 和 Canvas。
  2. 配置 Canvas: 使用 context.configure() 配置 Canvas,指定设备和纹理格式。
  3. 创建顶点缓冲区: 创建一个顶点缓冲区 vertexBuffer,存储三角形的顶点数据。
  4. 创建着色器模块: 创建一个着色器模块 shaderModule,包含顶点着色器和片元着色器。
  5. 创建渲染管线: 创建一个渲染管线 renderPipeline,它定义了如何渲染三角形。
  6. 渲染循环:render() 函数中,我们获取纹理视图、创建命令编码器、创建渲染通道编码器、设置管线和顶点缓冲区、绘制三角形、结束渲染通道、提交命令,然后循环渲染。

第六幕:高级技巧——BindGroup 和 Uniform

在实际应用中,我们经常需要将一些数据(比如模型矩阵、颜色、纹理)传递到着色器。这时,就可以使用 BindGroup 和 Uniform。

  • BindGroup: 用于将资源(比如缓冲区、纹理)绑定到着色器。
  • Uniform: 用于将常量数据传递到着色器。

咱们来修改一下上面的三角形程序,添加一个 Uniform,控制三角形的颜色。

async function main() {
  // 前面的代码不变,省略...

  // 6. 创建 Uniform 缓冲区
  const uniformBuffer = device.createBuffer({
    size: 16, // 4 个 float (RGBA)
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });

  // 初始颜色:绿色
  device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([0.0, 1.0, 0.0, 1.0]));

  // 7. 创建绑定组布局
  const bindGroupLayout = device.createBindGroupLayout({
    entries: [{
      binding: 0,
      visibility: GPUShaderStage.FRAGMENT,
      buffer: {
        type: "uniform",
      },
    }],
  });

  // 8. 创建绑定组
  const bindGroup = device.createBindGroup({
    layout: bindGroupLayout,
    entries: [{
      binding: 0,
      resource: {
        buffer: uniformBuffer,
      },
    }],
  });

  // 9. 创建渲染管线 (需要修改 layout)
  const renderPipeline = device.createRenderPipeline({
    layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
    vertex: {
      module: shaderModule,
      entryPoint: "vertexMain",
      buffers: [{
        arrayStride: 12,
        attributes: [{
          shaderLocation: 0,
          offset: 0,
          format: GPUVertexFormat.float32x3,
        }],
      }],
    },
    fragment: {
      module: shaderModule,
      entryPoint: "fragmentMain",
      targets: [{
        format: presentationFormat,
      }],
    },
    primitive: {
      topology: "triangle-list",
    },
  });

  // 修改后的着色器代码
  const shaderModule = device.createShaderModule({
    code: `
      @group(0) @binding(0) var<uniform> color: vec4<f32>;

      @vertex
      fn vertexMain(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
        return vec4<f32>(pos, 1.0);
      }

      @fragment
      fn fragmentMain() -> @location(0) vec4<f32> {
        return color; // 使用 Uniform 变量
      }
    `,
  });

  // 10. 渲染循环
  function render() {
    // 前面的代码不变,省略...

    // 11. 设置绑定组
    passEncoder.setBindGroup(0, bindGroup);

    // 12. 绘制三角形
    passEncoder.draw(3, 1, 0, 0);

    // 前面的代码不变,省略...
  }

  render();
}

main();

这段代码做了啥?

  1. 创建 Uniform 缓冲区: 创建了一个 Uniform 缓冲区 uniformBuffer,用于存储颜色数据。
  2. 创建绑定组布局: 创建了一个绑定组布局 bindGroupLayout,描述了绑定组的结构。
  3. 创建绑定组: 创建了一个绑定组 bindGroup,将 Uniform 缓冲区绑定到着色器。
  4. 修改渲染管线: 修改了渲染管线的布局,使用了绑定组布局。
  5. 修改着色器代码: 修改了着色器代码,从 Uniform 变量 color 中读取颜色数据。
  6. 设置绑定组: 在渲染循环中,使用 passEncoder.setBindGroup() 设置绑定组。

现在,三角形的颜色由 Uniform 变量控制,你可以通过修改 uniformBuffer 中的数据来改变三角形的颜色。

第七幕:性能优化——Command Buffer 和 Pipeline Cache

WebGPU 的性能优化也是一个重要的课题。这里介绍两个常用的技巧:

  • Command Buffer: 尽可能地将多个命令组合到一个 Command Buffer 中,减少 GPU 的状态切换。
  • Pipeline Cache: 缓存 Pipeline 对象,避免重复创建,提高渲染效率。

第八幕:总结与展望

今天咱们简单地聊了聊 WebGPU 的基本概念和用法,包括通用计算、图形渲染、BindGroup、Uniform 等等。WebGPU 是一门非常强大的技术,它可以让咱们在浏览器中实现各种高性能的应用。

当然,WebGPU 的学习曲线还是比较陡峭的,需要掌握一些底层的图形学知识。但只要你肯花时间学习,一定能掌握这门技术,创造出令人惊艳的作品。

WebGPU 的未来一片光明,它将成为 Web 开发的一个重要组成部分。相信在不久的将来,咱们就能看到更多基于 WebGPU 的精彩应用。

第九幕:互动与答疑

好了,今天的讲座就到这里。大家有什么问题可以提出来,咱们一块儿讨论讨论。

发表回复

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