WebGPU简介
WebGPU 是由 W3C GPU for the Web 工作组主导设计的新一代 Web 图形与计算 API,旨在为现代 GPU 硬件提供底层访问能力,同时兼顾跨平台兼容性(支持 Vulkan/Metal/DirectX 12 等原生接口)。相较于 WebGL,它通过显式资源管理、多线程渲染和低开销指令设计,实现了10倍级性能提升,并首次在 Web 平台原生支持通用 GPU 计算(GPGPU)。
WebGPU 的两个基本功能
1. 绘制三角形、点和线到纹理上(图形渲染)
使用WebGPU进行图形渲染的能力。在计算机图形学中,几乎所有复杂的图形和场景都可以通过基本的几何形状(如三角形)来构建。WebGPU允许开发者定义这些几何形状,并指定它们的颜色、材质、纹理等属性,然后将它们绘制到一个称为"纹理"的二维图像上。纹理可以被理解为存储图像数据的内存区域,它可以作为最终显示在屏幕上的图像的一部分,也可以作为进一步图形处理的输入。
2. 在 GPU 上进行计算
除了图形渲染外,WebGPU还支持通用计算(GPGPU,General-Purpose computing on Graphics Processing Units)。这意味着开发者可以编写程序,在GPU上执行非图形相关的计算任务。由于GPU设计用于并行处理大量数据,对于某些类型的计算密集型任务,比如物理模拟、机器学习、数据分析等,利用GPU可以实现显著的性能提升。
使用WebGPU的核心流程可分为三个阶段:
- 数据准备与传输:通过命令缓冲区(Command Buffer)将CPU端的几何数据、纹理、Uniform变量等资源传输至GPU显存,并建立缓冲区对象(如顶点缓冲区、统一缓冲区);
- 资源绑定与管线配置:将传输完成的资源按逻辑分组(Bind Group),通过管线布局(Pipeline Layout)与渲染管线绑定,确保着色器能按需访问资源;
- 着色器资源解析与计算:在WGSL着色器中通过`@group`和`@binding`语法声明资源引用,实现GPU端的并行计算与渲染(如顶点变换、片元着色)。
实践
下面通过实现一个简单的数字翻倍功能,感受一下WebGPU的基本流程
1.获取设备
在开始之前我们需要先获取逻辑设备GPUDevice
,逻辑设备是应用程序访问所有 WebGPU 功能的基础。
获取逻辑设备的一般流程如图所示:
WebGPU 中的 GPU
是浏览器中通过 navigator.gpu
访问的全局入口,作为连接浏览器与物理 GPU 的桥梁 ,它负责适配器(如硬件 GPU 或软件渲染器)的筛选,通过适配器(GPUAdapter
)可查询 GPU 硬件能力并创建逻辑设备(GPUDevice
),后者是应用与 GPU 交互的核心,隔离管理资源(缓冲区、纹理等)并提交计算/渲染指令。
wgpu
是基于 WebGPU 规范的跨平台图形与计算库,其核心类型 直接对应规范定义:
为了通过绘制一个简单的三角形来初步体验WebGPU的基本工作流程,我们首先需要获取逻辑设备 GPUDevice
。逻辑设备是应用程序访问所有 WebGPU 功能的基础。
接下来,我们可以开始实践,打印一些适配器的信息。
在 TypeScript 版本中(位于 ts-webgpu-learn/src/main.ts
文件内),代码如下:
typescript
import "./style.css";
async function main() {
// 1. 请求低功耗优先的GPU适配器(通常是集成显卡)
const adapter = await navigator?.gpu.requestAdapter({
powerPreference: "low-power",
});
// 2. 打印适配器信息:厂商/驱动等基础信息
console.log(adapter?.info);
// 3. 打印支持的硬件特性:如纹理压缩格式、计算着色器等
console.log(Array.from(adapter?.features ?? []));
// 4. 打印硬件限制:最大纹理尺寸、缓冲区大小等
console.log(adapter?.limits);
// 5. 通过适配器创建设备(设备是大部分API操作的入口)
const device = await adapter?.requestDevice();
if (!device) {
throw new Error("Failed to create device");
}
console.log(device); // 打印设备对象
}
main();
而在 Rust 版本中(位于 rs-wgpu-learn/src/main.rs
文件内),代码如下:
需要安装新的依赖pollster
,pollster
是一个轻量级的异步执行器(executor),用于阻塞当前线程直到 Future 完成 。当然也可以使用tokio
或者async-std
等其他异步运行时。
shell
cargo add pollster
rust
use log::info;
use wgpu::InstanceDescriptor;
fn main() -> anyhow::Result<()> {
// 初始化日志系统,仅显示INFO及以上级别日志
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
// 使用pollster运行异步代码块,类似tokio的block_on
pollster::block_on(run())?;
Ok(())
}
async fn run() -> anyhow::Result<()> {
// 创建wgpu实例,启用所有支持的图形后端(Vulkan/Metal/DX12等)
let instance = wgpu::Instance::new(&InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
info!("Instance created: {:#?}", instance); // 打印实例信息
// 请求低功耗优先的适配器,集成显卡优先
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
..Default::default()
})
.await
.ok_or(anyhow::anyhow!("No suitable adapter found!"))?;
// 打印适配器硬件信息、支持的特性和限制
info!("Adapter created: {:#?}", adapter.get_info());
info!("Adapter features: {:#?}", adapter.features());
info!("Adapter limits: {:#?}", adapter.limits());
// 创建设备和命令队列,设备是GPU操作的核心句柄
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
memory_hints: wgpu::MemoryHints::Performance, // 内存优化策略
..Default::default()
},
None, // 不指定追踪路径
)
.await?;
info!("Device created: {:#?}", device);
Ok(())
}
2.创建存储缓冲区
在WebGPU中,GPUBuffer
是用于存储原始数据的接口,而存储缓冲区(Storage Buffer)是其一种,专门用于在GPU显存中支持任意数据的读写操作,以便于着色器程序间高效共享和修改数据。
ts-webgpu-learn/src/main.ts
typescript
import "./style.css";
async function main() {
// ...
// 新增代码
const input = new Float32Array([1, 2, 3, 4]);
const storageBuffer = device.createBuffer({
label: "storage-buffer",
size: input.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
});
device?.queue.writeBuffer(storageBuffer, 0, input.buffer);
const resultBuffer = device?.createBuffer({
label: "result-buffer",
size: storageBuffer.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
}
main();
rs-wgpu-learn/src/main.rs
需要安装依赖bytemuck
。bytemuck
是一个专注于安全底层数据转换的库,其核心功能是通过零拷贝操作实现高效、可靠的数据类型转换和序列化。
shell
cargo add bytemuck -F derive
rust
// ...
async fn run() -> anyhow::Result<()> {
// ...
// 新增加
let input: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
let storage_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("storage_buffer"),
size: input.len() as u64 * std::mem::size_of::<f32>() as u64,
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
queue.write_buffer(&storage_buffer, 0, bytemuck::cast_slice(&input));
let result_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("result_buffer"),
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
size: storage_buffer.size(),
mapped_at_creation: false,
});
Ok(())
}
3.创建着色器模块
首先先编写WGSL着色器代码。
创建一个新的文件source/compute.wgsl
。
wgsl
// 定义一个存储缓冲区,绑定在组0,绑定点0
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
// 定义一个计算着色器,工作组大小为1
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) id: vec3u) {
// 将数据中的每个元素乘以2
data[id.x] *= 2.0;
}
然后创建着色器模块
ts-webgpu-learn/src/main.ts
typescript
import computeWgsl from "../../source/compute.wgsl?raw";
async function main() {
// ...
// 创建着色器模块
const shaderModel = device.createShaderModule({
label: "shader-model",
code: computeWgsl,
});
}
rs-wgpu-learn/src/main.rs
rust
async fn run() -> anyhow::Result<()> {
// ...
// 创建着色器模块
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("computer_shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../source/compute.wgsl").into()),
});
}
4.创建绑定组布局和绑定组
在WebGPU中,**绑定组布局(GPUBindGroupLayout
)是定义资源结构和访问规则的模板(如绑定索引、可见性、数据类型),而 绑定组(GPUBindGroup
)**是基于该模板实例化的资源集合,两者通过规范与实现分离的机制,实现高效资源管理与复用。
通过上面的着色器代码可知,我们将data
存储缓冲区绑定到了0号位。
ts-webgpu-learn/src/main.ts
typescript
// 创建绑定组布局时,为设备(device)调用createBindGroupLayout方法,并传入包含绑定信息的对象。
const bindGroupLayout = device.createBindGroupLayout({
label: "bind-group-layout", // 绑定组布局的标识符
entries: [
{
binding: 0, // 绑定点索引
visibility: GPUShaderStage.COMPUTE, // 指定该资源对计算着色器阶段可见
buffer: {
// 资源类型为存储缓冲区
type: "storage",
hasDynamicOffset: false, // 是否支持动态偏移
minBindingSize: 0, // 最小绑定大小
},
},
],
});
// 创建绑定组时,根据之前定义的绑定组布局(bindGroupLayout),通过device.createBindGroup方法将实际的存储缓冲区(storageBuffer)绑定到0号位。
const bindGroup = device.createBindGroup({
layout: bindGroupLayout, // 使用的绑定组布局
entries: [
{
binding: 0,
resource: {
// 实际绑定的资源
buffer: storageBuffer, // 存储缓冲区
},
},
],
});
rs-wgpu-learn/src/main.rs
rust
// 创建绑定组布局,描述了如何在着色器中访问绑定资源。
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0, // 绑定点索引
visibility: wgpu::ShaderStages::COMPUTE, // 计算着色器阶段可见
ty: wgpu::BindingType::Buffer {
// 绑定类型为存储缓冲区
ty: wgpu::BufferBindingType::Storage { read_only: false }, // 支持读写
has_dynamic_offset: false, // 不使用动态偏移
min_binding_size: wgpu::BufferSize::new(0), // 最小绑定大小
},
count: None,
}],
});
// 根据绑定组布局创建绑定组,将具体的存储缓冲区(storage_buffer)绑定到着色器程序中。
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bind_group"),
layout: &bind_group_layout, // 使用的绑定组布局
entries: &[wgpu::BindGroupEntry {
binding: 0, // 绑定点索引
resource: storage_buffer.as_entire_binding(), // 绑定整个存储缓冲区
}],
});
5.创建管道布局和计算管道
在WebGPU中,**管道布局(GPUPipelineLayout
)通过整合绑定组布局(GPUBindGroupLayout)定义资源在计算阶段的访问规则,而 计算管道(GPUComputePipeline
)**是专用于执行通用计算的管线类型,通过计算着色器(Compute Shader)实现GPU并行任务(如机器学习或物理模拟)的调度与执行。
ts-webgpu-learn/src/main.ts
ts
// 创建计算管道布局时,通过设备(device)调用createPipelineLayout方法,并传入包含绑定组布局(bindGroupLayouts)的数组。
const computePipelineLayout = device.createPipelineLayout({
label: "compute-pipeline-layout", // 计算管道布局的标识符
bindGroupLayouts: [bindGroupLayout], // 使用的绑定组布局列表
});
// 创建计算管道时,根据之前定义的计算管道布局(computePipelineLayout),通过device.createComputePipeline方法指定计算着色器模块(shaderModel)及其入口点(entryPoint)。
const computePipeline = device.createComputePipeline({
label: "compute-pipeline", // 计算管道的标识符
layout: computePipelineLayout, // 使用的计算管道布局
compute: {
module: shaderModel, // 计算着色器模块
entryPoint: "main", // 着色器程序入口点
},
});
rs-wgpu-learn/src/main.rs
rust
// 创建计算管道布局,描述了计算过程中如何访问绑定的资源。这里还指定了push_constant_ranges,但在当前示例中未使用。
let compute_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("compute_pipeline_layout"), // 计算管道布局的标识符
bind_group_layouts: &[&bind_group_layout], // 使用的绑定组布局引用列表
push_constant_ranges: &[], // 推送常量范围,当前未使用
});
// 根据计算管道布局创建计算管道,指定使用的计算着色器模块(shader)和入口点(entry_point)。
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("compute_pipeline"), // 计算管道的标识符
layout: Some(&compute_pipeline_layout), // 使用的计算管道布局
module: &shader, // 计算着色器模块
entry_point: Some("main"), // 着色器程序入口点
cache: None, // 管道缓存设置,当前未使用
compilation_options: Default::default(), // 编译选项,默认配置
});
6.创建命令编码器与计算通道
在WebGPU中,**命令编码器(GPUCommandEncoder
)负责记录GPU操作指令序列,而 计算通道(GPUComputePass
)**是专用于执行计算任务的指令集合单元。通过编码器将计算任务与数据拷贝操作打包为命令缓冲区后提交至队列执行。
ts-webgpu-learn/src/main.ts
typescript
// 创建命令编码器
const encoder = device.createCommandEncoder({
label: "compute-encoder",
});
const pass = encoder.beginComputePass({
label: "compute-pass",
});
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(input.length);
pass.end();
// 将存储缓冲区的数据复制到结果缓冲区
encoder.copyBufferToBuffer(
storageBuffer,
0,
resultBuffer,
0,
input.byteLength
);
// 提交命令缓冲区
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
rs-wgpu-learn/src/main.rs
rust
// 创建命令编码器
let mut encode = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
{
// 开始计算传递
let mut pass = encode.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("compute_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&compute_pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.dispatch_workgroups(input.len() as u32, 1, 1);
}
// 将存储缓冲区的数据复制到结果缓冲区
encode.copy_buffer_to_buffer(&storage_buffer, 0, &result_buffer, 0, storage_buffer.size());
// 提交命令缓冲区
let command_buffer = encode.finish();
queue.submit(std::iter::once(command_buffer));
注意:
在 Rust 的 wgpu
实现中,计算通道的生命周期由所有权系统自动管理 ,这一机制通过作用域规则和可变借用的排他性实现,无需手动调用 end()
。当创建 ComputePassEncoder
时,Rust 会通过 begin_compute_pass
方法获取父 CommandEncoder
的 独占可变借用 (&mut
)。如果省略包裹代码块的 {}
,会导致作用域重叠引发的借用冲突 ,调用copy_buffer_to_buffer
或者finish
将会导致编译失败。
7.映射读取计算结果
由于GPU显存与CPU内存物理隔离,需要通过异步映射机制 安全访问数据。mapAsync()
方法请求缓冲区映射权限,在GPU完成操作后触发回调。
ts-webgpu-learn/src/main.ts
typescript
// 映射结果缓冲区并读取数据
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange());
console.log("input", input);
console.log("result", result);
resultBuffer?.unmap();
rs-wgpu-learn/src/main.rs
rust
// 映射结果缓冲区并读取数据
result_buffer
.slice(..)
.map_async(wgpu::MapMode::Read, |res| {
info!("Mapping buffer ref {:?}", res);
});
device.poll(wgpu::Maintain::Wait);
let res = result_buffer.slice(..).get_mapped_range().to_vec();
let result: &[f32] = bytemuck::cast_slice(&res);
info!("Result: {:?}", result);
result_buffer.unmap();
注意:
为什么还需要一个resultBuffer
来查看数据,这是由于 GPU 显存与 CPU 内存物理隔离且缓冲区可能被 GPU 占用,需要通过 resultBuffer
中转数据,而 GPUBuffer 的映射机制 (如调用 mapAsync()
)通过异步权限请求,在 GPU 未操作缓冲区时允许 CPU 安全访问显存数据,避免直接读写导致的数据竞争或硬件错误。
总结
通过本文的学习,我们掌握了 WebGPU 的核心使用流程,并分别用 TypeScript 和 Rust 实现了数字翻倍功能。整个过程清晰展示了 WebGPU 的三个关键阶段:传输数据到 GPU → 绑定资源并配置管线 → 执行计算任务 。可以看出,GPU 编程的核心在于精准管理资源 (如显存分配、绑定组设置)和高效利用硬件并行能力。
在下一篇文章中,将通过 经典三角形绘制案例 ,介绍 WebGPU 的 图形渲染管线配置 与 顶点-片元着色器协同原理 。
参考: