WebGPU 基础 (WebGPU Fundamentals)

WebGPU 基础 (WebGPU Fundamentals)

本文将尝试向你教授 WebGPU 的最基本基础知识。

在阅读本文之前,默认你已经了解 JavaScript。本文将广泛使用数组映射(mapping arrays)、解构赋值(destructuring assignment)、展开运算符(spreading values)、async/await、es6 模块等概念。如果你还不了解 JavaScript 并想学习它,请参阅 JavaScript.info、Eloquent JavaScript 和/或 CodeCademy。

如果你已经了解 WebGL,请阅读这篇文章

WebGPU 是一个允许你执行 2 项基本操作的 API:

  1. 在纹理(textures)上绘制三角形/点/线
  2. 在 GPU 上运行计算(computations)

仅此而已!

除此之外,关于 WebGPU 的一切都取决于你。这就像学习 JavaScript、Rust 或 C++ 等计算机语言一样。首先你学习基础知识,然后由你创造性地利用这些基础知识来解决你的问题。

WebGPU 是一个极低级别的 API。虽然你可以制作一些简单的示例,但对于许多应用来说,它可能需要大量的代码和严的数据组织。例如,支持 WebGPU 的 three.js 包含约 550k 字节的压缩 JavaScript,而这仅仅是其基础库。这还不包括加载器(loaders)、控制器(controls)、后处理(post-processing)和许多其他功能。同样,TensorFlow 的核心加上 WebGPU 后端约为 600k 字节的压缩 JavaScript,且不包括对各种可选功能的持。

重点是,如果你只是想在屏幕上显示某些东西,最好选择一个能提供大量代码的库,因为如果你自己动手,就必须编写这些代码。

另一方面,也许你有自定义的使用场景,或者你想修改现有库,或者你只是好奇它是如何工作的。在这些情况下,请继续阅读!

入门 (Getting Started)

很难决定从哪里开始。在某种程度上,WebGPU 是一个非常简单的系统。它所做的只是在 GPU 上运行 3 种类型的函数:顶点着色器(Vertex Shaders)、片元着色器(Fragment Shaders)和计算着色器(Compute Shaders)。

顶点着色器计算顶点。着色器返回顶点位置。对于顶点着色器函数返回的每组 3 个顶点,都会在这 3 个位置之间绘制一个三角形。1

片元着色器计算颜色。2 当绘制三角形时,对于要绘制的每个像素,GPU 都会调用你的片元着色器。片元着色器随后返回一种颜色。

计算着色器更通用。它实际上只是一个你调用的函数,并说"执行这个函数 N 次"。GPU 在每次调用函数时都会传递迭代次数,因此你可以使用该数字在每次迭代中执行独特的操作。

如果你仔细观察,可以认为这些函数类似于传递给 array.forEacharray.map 的函数。你在 GPU 上运行的函数就是函数,就像 JavaScript 函数一样。不同之处在于它们运行在 GPU 上,因此为了运行它们,你需要将希望它们访问的所有数据以缓冲区(buffers)和纹理(textures)的形式复制到 GPU,并且它们只能输出到这些缓冲区和纹理。你需要在函数中指定函数将查找数据的绑定(bindings)或位置(locations)。而且,在 JavaScript 中,你需要将持有数据的缓冲区和纹理绑定到这些绑定或位置。完成这些操作后,你告诉 GPU 执行该函数。

关于这张图需要注意的地方:

  • 有一个 管线(Pipeline) 。它包含了 GPU 将运行的顶点着色器和片元着色器。你也可以拥有包含计算着色器的管线。
  • 着色器通过 绑定组(Bind Groups) 间接引用资源(缓冲区、纹理、采样器)。
  • 管线定义了通过内部状态间接引用缓冲区的属性(Attributes)。
  • 属性从缓冲区中提取数据并将其输入到顶点着色器中。
  • 顶点着色器可能会将数据输入到片元着色器中。
  • 片元着色器通过渲染通道描述(render pass description)间接写入纹理。

要在 GPU 上执行着色器,你需要创建所有这些资源并设置这些状态。资源的创建相对直接。有趣的一点是,大多数 WebGPU 资源在创建后不能更改。你可以更改它们的内容,但不能更改它们的大小、用法、格式等。如果你想更改这些内容,你需要创建一个新资源并销毁旧资源。

某些状态是通过创建并执行 命令缓冲区(command buffers) 来设置的。命令缓冲区正如其名,它们是命令的缓冲区。你创建 命令编码器(command encoders) 。编码器将命令编码到命令缓冲区中。然后你完成编码器,它会返回创建的命令缓冲区。接着你可以提交该命令缓冲区,让 WebGPU 执行这些命令。

以下是编码命令缓冲区的一些伪代码,以及生成的命令缓冲区的表示:

JavaScript

js 复制代码
encoder = device.createCommandEncoder()
// 绘制某些东西
{
  pass = encoder.beginRenderPass(...)
  pass.setPipeline(...)
  pass.setVertexBuffer(0, ...)
  pass.setVertexBuffer(1, ...)
  pass.setIndexBuffer(...)
  pass.setBindGroup(0, ...)
  pass.setBindGroup(1, ...)
  pass.draw(...)
  pass.end()
}
// 绘制其他东西
{
  pass = encoder.beginRenderPass(...)
  pass.setPipeline(...)
  pass.setVertexBuffer(0, ...)
  pass.setBindGroup(0, ...)
  pass.draw(...)
  pass.end()
}
// 计算某些东西
{
  pass = encoder.beginComputePass(...)
  pass.setBindGroup(0, ...)
  pass.setPipeline(...)
  pass.dispatchWorkgroups(...)
  pass.end();
}
commandBuffer = encoder.finish();

一旦创建了命令缓冲区,你就可以提交它来执行:

JavaScript

ini 复制代码
device.queue.submit([commandBuffer]);

前面显示的"WebGPU 设置简化图"表示命令缓冲区中单个绘制命令的状态。执行命令将设置内部状态,然后绘制命令将告诉 GPU 执行顶点着色器(并间接执行片元着色器)。dispatchWorkgroup 命令将告诉 GPU 执行计算着色器。

我希望这能让你对需要设置的状态有一些心理映射。如上所述,WebGPU 可以做 2 件基本事情:

  1. 在纹理上绘制三角形/点/线
  2. 在 GPU 上运行计算

我们将详细讲解执行这两件事的小示例。其他文章将展示向这些事物提供数据的各种方法。请注意,这将非常基础。我们需要建立这些基础。稍后我们将展示如何使用它们来执行人们通常使用 GPU 执行的操作,如 2D 图形、3D 图形等。

在纹理上绘制三角形 (Drawing triangles to textures)

WebGPU 可以将三角形绘制到纹理上。就本文而言,纹理是像素的 2D 矩形。3 <canvas> 元素代表网页上的一个纹理。在 WebGPU 中,我们可以向画布请求一个纹理,然后渲染到该纹理。

为了使用 WebGPU 绘制三角形,我们必须提供 2 个"着色器"。再次强调,着色器是运行在 GPU 上的函数。这两个着色器是:

  1. 顶点着色器 (Vertex Shaders) :计算用于绘制三角形/线/点的顶点位置的函数。
  2. 片元着色器 (Fragment Shaders) :计算在绘制三角形/线/点时,要绘制/光栅化的每个像素的颜色(或其他数据)的函数。

让我们从一个非常小的 WebGPU 程序开始画一个三角形。

我们需要一个画布来显示我们的三角形:

HTML 复制代码
<canvas></canvas>

然后我们需要一个 <script> 标签来存放我们的 JavaScript:

HTML 复制代码
<canvas></canvas>
<script type="module">
  ... javascript goes here ...
</script>

下面的所有 JavaScript 都将放在这个 script 标签内。

WebGPU 是一个异步 API,因此在异步函数中使用它是最简单的。我们首先请求一个适配器(adapter),然后从适配器请求一个设备(device)。

JavaScript 复制代码
async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();
  if (!device) {
    fail('need a browser that supports WebGPU');
    return;
  }
}
main();

上面的代码相当直白。首先,我们使用 ?. 可选链操作符请求适配器,这样如果 navigator.gpu 不存在,适配器将是 undefined。如果它存在,我们将调用 requestAdapter。它异步返回结果,所以我们需要 await。适配器代表特定的 GPU。某些设备有多个 GPU。

从适配器中,我们请求设备,同样使用 ?.,这样如果适配器恰好是 undefined,设备也将是 undefined。如果设备未设置,可能是用户使用了旧浏览器。

接下来,我们查找画布并为其创建 webgpu 上下文。这将让我们获得一个要渲染到的纹理。该纹理将用于在网页中显示画布。

JavaScript 复制代码
// 从画布获取 WebGPU 上下文并进行配置
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
  device,
  format: presentationFormat,
});

同样,上面的代码非常直白。我们从画布获取 "webgpu" 上下文。我们询问系统首选的画布格式是什么。这将是 "rgba8unorm" 或 "bgra8unorm"。它是什么并不重要,但查询它会使用户的系统运行得更快。

我们将该格式作为 format 通过调用 configure 传递到 webgpu 画布上下文中。我们还传入了 device,这将此画布与我们刚刚创建的设备关联起来。

接下来,我们创建一个着色器模块。着色器模块包含一个或多个着色器函数。在我们的例子中,我们将创建一个顶点着色器函数和一个片元着色器函数。

JavaScript 复制代码
const module = device.createShaderModule({
  label: 'our hardcoded red triangle shaders',
  code: /* wgsl */ `
    @vertex
    fn vs(
      @builtin(vertex_index) vertexIndex : u32
    ) -> @builtin(position) vec4f {
      let pos = array(
        vec2f( 0.0,  0.5),  // 顶部中心
        vec2f(-0.5, -0.5),  // 左下角
        vec2f( 0.5, -0.5)   // 右下角
      );
      return vec4f(pos[vertexIndex], 0.0, 1.0);
    }

    @fragment
    fn fs() -> @location(0) vec4f {
      return vec4f(1.0, 0.0, 0.0, 1.0);
    }
  `,
});

着色器是用一种称为 WebGPU 着色语言 (WGSL) 的语言编写的,通常发音为 wig-sil。WGSL 是一种强类型语言,我们将在另一篇文章中尝试更详细地介绍。目前,我希望通过一些解释,你可以推断出一些基础知识。

注意:在本网站中,存储 WGSL 的字符串前面都有 /* wgsl */ 注释。这是一种约定,旨在帮助文本编辑器尝试对 WGSL 进行语法高亮和/或提供智能提示。

上面我们看到一个名为 vs 的函数使用了 @vertex 属性声明。这指定它为一个顶点着色器函数。

Rust 复制代码
@vertex
fn vs(
  @builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
  ...

它接受一个我们命名为 vertexIndex 的参数。vertexIndex 是一个 u32,意思是 32 位无符号整数。它从名为 vertex_index 的内置变量(builtin)中获取值。vertex_index 就像一个迭代次数,类似于 JavaScript 的 Array.map(function(value, index) { ... }) 中的 index。如果我们通过调用 draw 告诉 GPU 执行此函数 10 次,第一次 vertex_index 将为 0,第二次为 1,第三次为 2,依此类推。4

我们的 vs 函数声明返回一个 vec4f,它是四个 32 位浮点值的向量。可以把它看作一个包含 4 个值的数组或一个具有 4 个属性的对象,如 {x: 0, y: 0, z: 0, w: 0}。此返回值将被分配给 position 内置变量。在"三角形列表"(triangle-list)模式下,顶点着色器每执行 3 次,就会连接我们返回的 3 个位置值绘制一个三角形。

WebGPU 中的位置需要返回到 裁剪空间(clip space) 中,其中 X 从左侧的 -1.0 到右侧的 +1.0,Y 从底部的 -1.0 到顶部的 +1.0。无论我们要绘制的纹理大小如何,这都是正确的。

vs 函数声明了一个由 3 个 vec2f 组成的数组。每个 vec2f 由两个 32 位浮点值组成。

Rust 复制代码
let pos = array(
  vec2f( 0.0,  0.5),  // 顶部中心
  vec2f(-0.5, -0.5),  // 左下角
  vec2f( 0.5, -0.5)   // 右下角
);

最后,它使用 vertexIndex 从数组中返回 3 个值之一。由于该函数要求返回类型为 4 个浮点值,且由于 posvec2f 数组,因此代码为剩余的 2 个值提供了 0.0 和 1.0。

Rust 复制代码
return vec4f(pos[vertexIndex], 0.0, 1.0);

请注意,对于在 2D 中绘制内容,我们通常只需要位置的 x 和 y 值。z 值用于深度测试(depth testing),将在正交投影文章中提到。w 值用于透视除法(perspective divide),将在透视投影文章中提到。目前,将 z 设置为 0.0,将 w 设置为 1.0 是我们绘制三角形所需的。

着色器模块还声明了一个名为 fs 的函数,该函数使用 @fragment 属性声明,使其成为片元着色器函数。

Rust 复制代码
@fragment
fn fs() -> @location(0) vec4f {

此函数不接受任何参数,并在 location(0) 返回一个 vec4f。这意味着它将写入第一个渲染目标。稍后我们将使第一个渲染目标成为我们的画布纹理。

Rust 复制代码
return vec4f(1, 0, 0, 1);

代码返回 1, 0, 0, 1,即红色。WebGPU 中的颜色通常指定为 0.0 到 1.0 的浮点值,其中上述 4 个值分别对应红、绿、蓝和阿尔法(alpha)。

当 GPU 对三角形进行光栅化(用像素绘制)时,它将调用片元着色器以找出每个像素的颜色。在我们的例子中,我们只是返回红色。

还需要注意的一点是 label。几乎每个 WebGPU 对象都可以带有一个标签。标签完全是可选的,但最好给它们贴上标签。当发生错误时,大多数 WebGPU 错误会显示引发错误的对象的标签。在包含 100 个着色器模块、100 个管线、100 个缓冲区的程序中,如果没有标签,你可能会收到类似"着色器模块发生错误"的错误,这将需要大量工作才能找出具体是哪一个。如果给它们贴上标签,你会得到类似"着色器模块 '我们的硬编码红色三角形着色器' 发生错误"的错误,这更具描述性。

现在我们有了着色器模块,接下来需要创建一个渲染管线。

JavaScript 复制代码
const pipeline = device.createRenderPipeline({
  label: 'our hardcoded red triangle pipeline',
  layout: 'auto',
  vertex: {
    module,
    entryPoint: 'vs',
  },
  fragment: {
    module,
    entryPoint: 'fs',
    targets: [{ format: presentationFormat }],
  },
});

在这种情况下,没有太多要设置的。我们将 layout 设置为 'auto',这意味着我们希望 WebGPU 从着色器中派生数据的布局。不过我们没有使用任何数据。

然后我们告诉渲染管线为顶点着色器使用着色器模块中的 vs 函数,为片元着色器使用 fs 函数。此外,我们告诉它第一个渲染目标的格式。"渲染目标"意味着我们将要渲染到的纹理。当我们创建管线时,我们必须指定最终将使用此管线进行渲染的纹理的格式。

targets 数组的元素 0 对应于我们在片元着色器返回值中指定的 location 0。稍后,我们将把该目标设置为画布的纹理。

一个快捷方式是,对于每个着色阶段 vertexfragment,如果对应类型只有一个函数,则无需指定 entryPoint。WebGPU 将使用与着色阶段匹配的唯一函数。因此我们可以简化上面的代码。

接下来,我们准备一个 GPURenderPassDescriptor,它描述了我们要绘制到哪些纹理以及如何使用它们。

JavaScript 复制代码
const renderPassDescriptor = {
  label: 'our basic canvas renderPass',
  colorAttachments: [
    {
      // view: <- 渲染时填充
      clearValue: [0.3, 0.3, 0.3, 1],
      loadOp: 'clear',
      storeOp: 'store',
    },
  ],
};

GPURenderPassDescriptor 有一个 colorAttachments 数组,其中列出了我们要渲染到的纹理以及如何处理它们。我们将稍后填充实际要渲染到的纹理。目前,我们设置了一个半深灰色的清除值,以及 loadOpstoreOploadOp: 'clear' 指定在绘制之前将纹理清除为清除值。另一个选项是 'load',这意味着将纹理的现有内容加载到 GPU 中,以便我们可以绘制在已有内容之上。storeOp: 'store' 意味着存储我们绘制的结果。我们也可以传递 'discard',这将丢弃我们绘制的内容。我们将在另一篇文章中讨论为什么要这样做。

现在是渲染的时候了。

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.draw(3);  // 调用顶点着色器 3 次
  pass.end();

  // 完成编码并提交命令
  const commandBuffer = encoder.finish();
  device.queue.submit([commandBuffer]);
}

render();

首先,我们通过调用 context.getCurrentTexture().createView() 获取画布的当前纹理视图。通过调用 context.getCurrentTexture(),我们正在获取一个将显示在网页画布中的纹理。我们还调用 createView。你可以从纹理的一部分创建视图,但在没有任何参数的情况下,它将返回最常见的默认视图。我们需要设置 colorAttachments[0].view

接下来,我们创建一个命令编码器。然后通过调用 encoder.beginRenderPass(renderPassDescriptor) 创建一个渲染通道(render pass)。渲染通道会执行我们的渲染命令。我们在 renderPassDescriptor 中传入了颜色附件,因此它将开始通过清除纹理进行渲染。

我们设置管线,然后调用 draw。由于我们将 3 传递给 draw,我们的顶点着色器将被调用 3 次,vertex_index 将分别为 0、1 和 2。由于我们的顶点着色器在每次执行时返回不同的位置,因此每组 3 个位置将产生一个三角形。

最后,我们结束通道,完成编码器以获得命令缓冲区,并提交命令缓冲区。

运行该程序,我们得到一个三角形。

Triangle Demo Placeholder

GPU 计算 (Running computations on the GPU)

接下来让我们看看如何利用 GPU 进行计算。

我们将使用一个简单的例子:取一些数字并将它们翻倍。

首先,我们需要一个计算着色器。

JavaScript 复制代码
const module = device.createShaderModule({
  label: 'doubling compute shader',
  code: /* wgsl */ `
    @group(0) @binding(0) var<storage, read_write> data: array<f32>;

    @compute @workgroup_size(1)
    fn main(@builtin(global_invocation_id) id: vec3u) {
      data[id.x] = data[id.x] * 2.0;
    }
  `,
});

在这个着色器中,我们声明了一个名为 data 的变量。

Rust 复制代码
@group(0) @binding(0) var<storage, read_write> data: array<f32>;

它被赋予了 @group(0)@binding(0)。它被声明为 var<storage, read_write>,这意味着它将被存储在缓冲区中,并且它是可读写的。它被定义为 array<f32>,即 32 位浮点数的数组。

然后我们定义了函数 main

Rust 复制代码
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) id: vec3u) {

我们赋予它 @compute 属性,使其成为计算着色器。我们还赋予它 @workgroup_size(1) 属性,我们将在另一篇文章中讨论它的含义。

它接受一个参数 idid 是一个 vec3u 类型,由三个 32 位无符号整数组成。它通过内置变量 global_invocation_id 获取它的值。如果你仔细观察,你可以认为这就像我们在上面谈到的 vertex_index。如果我们告诉 GPU 运行此函数 10 次,那么在第一次运行中 id.x 将为 0,第二次为 1,第三次为 2,依此类推。

代码本身非常简单:

Rust 复制代码
data[id.x] = data[id.x] * 2.0;

它使用 id.x 索引到我们的数组中,并将值乘以 2。

现在我们有了着色器,我们需要创建一个计算管线。

JavaScript 复制代码
const pipeline = device.createComputePipeline({
  label: 'doubling compute pipeline',
  layout: 'auto',
  compute: {
    module,
    entryPoint: 'main',
  },
});

正如我们之前所做的,我们将 layout 设置为 'auto'

接下来,我们需要一些数据。

JavaScript 复制代码
const input = new Float32Array([1, 3, 5]);

由于数据是在 JavaScript 端(CPU 端),我们需要在 GPU 端创建一个缓冲区,并将数据从 JavaScript 复制到 GPU 缓冲区。

JavaScript 复制代码
// 在 GPU 上创建一个缓冲区来保存数据
const workBuffer = device.createBuffer({
  label: 'work buffer',
  size: input.byteLength,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
// 将数据复制到缓冲区
device.queue.writeBuffer(workBuffer, 0, input);

通过传递 GPUBufferUsage.STORAGE,我们说希望该缓冲区可被用作存储。这使其与着色器中的 var<storage,...> 兼容。此外,我们希望能够将数据复制到此缓冲区,因此我们包含了 GPUBufferUsage.COPY_DST 标志。最后,我们希望能够从缓冲区复制数据,因此我们包含了 GPUBufferUsage.COPY_SRC

请注意,你无法直接从 JavaScript 读取 WebGPU 缓冲区的内容。相反,你必须对其进行"映射"(map),这是另一种向 WebGPU 请求访问缓冲区的方式,因为缓冲区可能正在使用中,并且它可能仅存在于 GPU 上。

可以映射到 JavaScript 的 WebGPU 缓冲区不能用于太多其他用途。换句话说,我们不能直接映射上面创建的缓冲区,如果我们尝试添加标志使其可映射,我们将收到一个错误,因为它与用法 STORAGE 不兼容。

因此,为了看到计算结果,我们需要另一个缓冲区。运行计算后,我们将把上面的缓冲区复制到这个结果缓冲区中,并设置其标志以便我们可以对其进行映射。

JavaScript 复制代码
// 在 GPU 上创建一个缓冲区来获取结果的副本
const resultBuffer = device.createBuffer({
  label: 'result buffer',
  size: input.byteLength,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});

MAP_READ 意味着我们希望能够映射此缓冲区以读取数据。

为了告诉我们的着色器我们要处理的缓冲区,我们需要创建一个绑定组(bindGroup)。

JavaScript 复制代码
// 设置一个绑定组来告诉着色器使用哪个
// 缓冲区进行计算
const bindGroup = device.createBindGroup({
  label: 'bindGroup for work buffer',
  layout: pipeline.getBindGroupLayout(0),
  entries: [
    { binding: 0, resource: { buffer: workBuffer } },
  ],
});

我们从管线获取绑定组的布局。然后设置绑定组条目。pipeline.getBindGroupLayout(0) 中的 0 对应着色器中的 @group(0)。条目中的 {binding: 0 ... 对应着色器中的 @group(0) @binding(0)

现在我们可以开始编码命令。

JavaScript 复制代码
// 编码执行计算的命令
const encoder = device.createCommandEncoder({
  label: 'doubling encoder',
});
const pass = encoder.beginComputePass({
  label: 'doubling compute pass',
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(input.length);
pass.end();

我们创建一个命令编码器。开始一个计算通道。设置管线,然后设置绑定组。这里,pass.setBindGroup(0, bindGroup) 中的 0 对应着色器中的 @group(0)。然后我们调用 dispatchWorkgroups,在这种情况下,我们传入 input.length(即 3),告诉 WebGPU 运行计算着色器 3 次。然后结束通道。

这是执行 dispatchWorkgroups 时的情况。

计算完成后,我们要求 WebGPU 从 workBuffer 复制到 resultBuffer

JavaScript 复制代码
// 编码将结果复制到可映射缓冲区的命令。
encoder.copyBufferToBuffer(workBuffer, 0, resultBuffer, 0, resultBuffer.size);

现在我们可以完成编码器以获得命令缓冲区,然后提交该命令缓冲区。

JavaScript 复制代码
// 完成编码并提交命令
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);

然后我们映射结果缓冲区并获取数据的副本。

JavaScript 复制代码
// 读取结果
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange());
console.log('input', input);
console.log('result', result);
resultBuffer.unmap();

要映射结果缓冲区,我们调用 mapAsync 并必须等待它完成。映射后,我们可以调用 resultBuffer.getMappedRange(),在不带参数的情况下它将返回整个缓冲区的 ArrayBuffer。我们将其放入 Float32Array 类型数组视图中,然后查看这些值。一个重要的细节是,getMappedRange 返回的 ArrayBuffer 仅在调用 unmap 之前有效。在 unmap 之后,它的长度将被设置为 0,其数据将不再可访问。

运行该程序,我们可以看到我们得到了结果,所有的数字都翻倍了。

Compute Demo Placeholder

我们将在其他文章中介绍如何真正使用计算着色器。目前,希望你已经对 WebGPU 的作用有了初步的了解。除此之外的一切都取决于你!将 WebGPU 视为类似于其他编程语言。它提供了一些基本功能,其余的留给你的创造力。

使 WebGPU 编程特别的是,这些函数(顶点着色器、片元着色器和计算着色器)运行在你的 GPU 上。GPU 可能拥有超过 10,000 个处理器,这意味着它们可以并行进行 10,000 次以上的计算,这可能比你的 CPU 并行计算能力高出 3 个或更多数量级。

(后略:关于画布调整大小等细节)

相关推荐
猫猫不是喵喵.6 分钟前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~1 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华2 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子4 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅4 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni5 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学6 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师6 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc886 小时前
微软EDGE浏览器功能学习
前端·学习·edge