Part 2:WebGPU基本对象及其创建
WebGPU基本对象及其创建
摘要:本文详细介绍了 WebGPU 核心对象的创建流程与作用。首先通过浏览器 Navigator.gpu 检测支持性并获取 GPUAdapter,进而创建 GPUDevice 作为与 GPU 的逻辑连接。GPUDevice 用于创建各类资源(缓冲区、纹理、管线等)及 GPUCommandEncoder 来编码指令。指令缓冲区通过 GPUQueue 提交执行。若需图形渲染,还需将 GPUDevice 与 HTML Canvas 元素关联:获取 Canvas 上下文并配置纹理格式。文章最后给出了完整的初始化代码示例,涵盖从检测支持到画布配置的全过程。
- Part 1:WebGPU的因承和编程范式
📑 目录
- 总体流程
- Navigator
- GPUDevice
- GPUCommandEncoder
- GPUQueue
- Canvas元素
- Canvas Context
- 一个完整的初始化流程
总体流程
设备 GPU 和运行 WebGPU API 的 Web 浏览器之间有几个抽象层。在开始学习 WebGPU 时了解这些抽象层很有用
WebGPU操作的核心范式是:将指令和数据打包为指令缓冲区(command buffer)对象,提交到与GPU关联的队列(queue)中执行。完成这一核心流程至少需要以下六个步骤:
- 访问浏览器的
Navigator对象,检查其是否支持WebGPU; - 若支持,通过
Navigator获取GPUAdapter(适配器); - 使用
GPUAdapter获取GPUDevice(设备的逻辑连接); - 使用
GPUDevice创建GPUCommandEncoder(指令编码器); - 配置该编码器,将具体指令录入到指令缓冲区;
- 将指令缓冲区提交到与
GPUDevice关联的GPUQueue。
如果应用还需要在页面中进行图形渲染(而不仅仅是通用计算),则还需要将GPUDevice与页面中的<canvas>元素关联起来,这需要额外的三个步骤:
- 在HTML文档中访问
<canvas>元素; - 通过该元素获取
GPUCanvasContext(画布上下文); - 使用
GPUDevice及所需的纹理格式对GPUCanvasContext进行配置。
完成上述关联后,应用便可以通过GPUDevice发出的渲染指令,将图形结果绘制到画布上。下面依次介绍流程中涉及的各个核心对象。
Navigator
现代浏览器都提供全局的Navigator对象,用于暴露浏览器自身的能力信息。例如clipboard属性可用于读取系统剪贴板,connection属性用于检测网络连接状况,deviceMemory属性用于查看可用内存等。
如果当前浏览器支持WebGPU,Navigator对象上会存在一个gpu属性(类型为GPU)。因此可以用以下代码来检测浏览器是否支持WebGPU:
// 检查浏览器是否支持WebGPUif(!navigator.gpu){thrownewError("浏览器不支持WebGPU。");}gpu属性提供了两个重要的方法:
| 方法 | 说明 |
|---|---|
requestAdapter() | 返回一个Promise,解析为一个GPUAdapter对象(若失败则解析为null) |
getPreferredCanvasFormat() | 返回一个字符串,表示当前浏览器/设备下最适合用于画布渲染的纹理格式 |
需要特别注意:浏览器支持WebGPU这一API,并不代表客户端硬件一定支持WebGPU所需的渲染与计算能力。必须实际调用requestAdapter()来进行确认,如:
// 访问GPU适配器constadapter=awaitnavigator.gpu.requestAdapter();if(!adapter){thrownewError("没有找到合适的GPUAdapter。");}GPUAdapter代表了对客户端某个物理(或软件模拟的)GPU实现的一次抽象访问,它本身并不直接执行渲染或计算,而是用来查询设备能力,并进一步获取GPUDevice。
关于适配器信息:在早期版本的规范中,需要通过异步方法adapter.requestAdapterInfo()获取设备信息;在当前的WebGPU标准中,这一信息已改为通过只读属性adapter.info(类型为GPUAdapterInfo)同步获取,不再需要await。GPUAdapterInfo包含以下只读字段:
| 属性 | 类型 | 描述 |
|---|---|---|
| vendor | String | GPU的制造商标识,如nvidia、amd、intel |
| device | String | 设备名称标识 |
| architecture | String | 设备架构标识 |
| description | String | 适配器的描述信息 |
这些信息在应用需要针对不同厂商GPU执行不同代码路径(例如规避某些驱动缺陷)时非常有用。
GPUAdapter还有两个额外的属性:
features是一个GPUSupportedFeatures对象(类似Set的结构),列出了该GPU所支持的可选特性limits是一个GPUSupportedLimits对象,描述了GPU在各项操作参数上的最大/最小取值范围
可以使用adapter.features.has(featureName)判断设备是否支持某项特性,也可以直接遍历features集合获取全部已支持特性。下表列出了WebGPU中常见的可选特性:
| 特性ID | 描述 |
|---|---|
depth-clip-control | 允许应用使用类似OpenGL的深度范围[-1, 1],而非WebGPU默认的[0, 1] |
depth32float-stencil8 | 深度/模板缓冲区中的元素可以是32位浮点深度值加8位模板值 |
texture-compression-bc | 支持使用块压缩(BC)算法压缩的纹理 |
texture-compression-etc2 | 支持使用Ericsson纹理压缩2(ETC2)算法压缩的纹理 |
texture-compression-astc | 支持使用自适应可伸缩纹理压缩(ASTC)算法压缩的纹理 |
indirect-first-instance | 允许间接绘制调用(indirect draw)中的firstInstance参数设置为大于0的值 |
shader-f16 | 着色器中可以访问16位半精度浮点数(f16) |
rg11b10ufloat-renderable | 允许将rg11b10ufloat用作可渲染的纹理格式 |
bgra8unorm-storage | 允许创建像素格式为bgra8unorm的存储纹理(storage texture) |
float32-filterable | GPU可以对32位浮点纹理进行线性过滤(平滑处理) |
更多可用功能见MDN GPUSupportedFeatures文档。当然,GPUAdapter最重要的作用还是提供访问GPUDevice。
GPUDevice
可以通过以下代码获取GPUDevice:
// 访问GPUconstdevice=awaitadapter.requestDevice();if(!device){thrownewError("创建GPUDevice失败。");}GPUDevice代表了应用与客户端GPU之间的一条逻辑连接,在WebGPU开发中处于绝对核心的位置。几乎所有WebGPU资源与操作对象都是通过它的方法创建的,包括但不限于:
createBuffer():创建缓冲区(buffer)资源;createTexture()/createSampler():创建纹理与采样器;createShaderModule():创建着色器模块;createBindGroup()/createBindGroupLayout():创建资源绑定组及其布局;createRenderPipeline()/createComputePipeline():创建渲染管线与计算管线;createCommandEncoder():创建指令编码器。
此外,GPUDevice还继承自EventTarget,因此可以通过device.addEventListener("uncapturederror", ...)监听未被捕获的错误;同时它还提供了一个device.lost(Promise),可用于监控设备连接意外丢失的情况。这些细节超出了本章范围,后续章节会陆续展开。
GPUCommandEncoder
GPUCommandEncoder负责创建并临时存储将要发送给GPU的一系列指令,它通过调用GPUDevice上的方法创建:
// 创建command encoderencoder=device.createCommandEncoder();if(!encoder){thrownewError("创建GPUCommandEncoder失败。");}有了这个对象,就可以定义要发送给设备执行的具体操作了。它的方法可以分为两大类:
(1)开启子编码器 / 完成编码(最常用的三个方法):
| 方法 | 描述 |
|---|---|
beginRenderPass() | 开始编码一个渲染通道,返回GPURenderPassEncoder,用于定义图形渲染相关操作 |
beginComputePass() | 开始编码一个计算通道,返回GPUComputePassEncoder,用于定义通用计算相关操作 |
finish() | 结束该编码器全部指令的录入,返回一个GPUCommandBuffer,随后即可提交给设备队列执行 |
(2)资源操作与调试辅助方法(除以上三个核心方法外,GPUCommandEncoder还提供了下列方法,用于在缓冲区/纹理之间拷贝数据,以及插入调试信息):
| 方法 | 描述 |
|---|---|
clearBuffer() | 将缓冲区内容清零 |
copyBufferToBuffer() | 在两个缓冲区之间拷贝数据 |
copyBufferToTexture() | 将缓冲区数据拷贝到纹理 |
copyTextureToBuffer() | 将纹理数据拷贝到缓冲区 |
copyTextureToTexture() | 在两个纹理之间拷贝数据 |
pushDebugGroup()/popDebugGroup() | 开启/结束一个带标签的调试分组,便于在调试工具中定位指令 |
insertDebugMarker() | 在指令序列中插入一条调试标记 |
resolveQuerySet() | 将查询集合(如时间戳查询、遮挡查询)的结果写入缓冲区 |
writeTimestamp() | 向查询集合写入一个时间戳(用于性能分析) |
其中,finish()是每个非平凡WebGPU应用都必须调用的方法:它将编码器已录入的全部数据打包为一个GPUCommandBuffer。一旦这个缓冲区被提交到设备队列,GPU便会按顺序开始执行其中的指令。
GPUQueue
GPUDevice还有一个重要的只读属性queue,用于访问与该设备关联的GPUQueue。应用需要显式调用队列上的方法,才能真正触发GPU执行操作。其常用方法包括:
writeBuffer(buffer, bufferOffset, data, ...):直接向某个GPU缓冲区写入(更新)数据;writeTexture(destination, data, dataLayout, size):直接向某个GPU纹理写入(更新)数据;submit(commandBuffers):将一个或多个指令缓冲区提交给GPU执行。
调用submit()是整个流程中最关键的一步——GPU会依据其中的指令,真正执行图形渲染或通用计算任务。提交之后,应用还可以调用queue.onSubmittedWorkDone()(返回一个Promise),用来在队列中已提交的全部工作执行完毕后得到通知,从而监控执行进度。
Canvas元素
<canvas>是用于承载图形显示的HTML元素。当WebGPU执行渲染操作时,最终生成的图形会显示在与之关联的画布中。因此,若要用WebGPU进行图形渲染,通常需要以下三个步骤:
- 在HTML文档中定义一个
<canvas>元素; - 在JavaScript中获取该元素对应的
HTMLCanvasElement对象; - 从该元素获取一个渲染上下文,并对其进行配置。
默认情况下,<canvas>元素显示为一块空白矩形。要设置其显示内容,需要先在JavaScript中获取该元素——既可以通过标签选择(document.querySelector("canvas")),也可以通过ID选择(document.getElementById(id))——随后调用该元素上的getContext()方法,获得一个用于绘制内容的上下文对象。
Canvas Context
需要注意,WebGPU只是设置<canvas>显示内容的众多方式之一,此外还可以使用WebGL、2D位图绘制API或简单的2D绘图上下文("2d")。因此需要在调用getContext()时明确指定所需的上下文类型。对于WebGPU应用,应传入字符串"webgpu":
constcanvas=document.getElementById("canvas_example");constcontext=canvas.getContext("webgpu");由此得到的context对象类型为GPUCanvasContext,它提供了一个关键方法——configure()——用于设置渲染上下文的具体行为。传给configure()的配置对象(GPUCanvasConfiguration)主要包含以下字段:
| 属性 | 是否必需 | 描述 |
|---|---|---|
device | 是 | 用于控制该画布渲染的GPUDevice |
format | 是 | 从画布中获取的纹理所使用的像素格式(决定了WebGPU应以何种像素格式向画布输出内容) |
usage | 否 | 描述从画布获取的纹理将如何被使用(如GPUTextureUsage.RENDER_ATTACHMENT),默认已包含渲染附件用途 |
alphaMode | 否 | 控制透明度/合成方式("opaque"或"premultiplied") |
colorSpace | 否 | 获取纹理的色彩空间 |
viewFormats | 否 | 允许创建的附加视图格式数组 |
其中最核心的是device与format这两个属性:device应设置为前面获取的GPUDevice;format则决定了WebGPU向画布输出内容时采用的像素格式。由于不同硬件/操作系统所支持的最佳纹理格式并不完全相同,通常不需要手动指定,而是通过调用navigator.gpu.getPreferredCanvasFormat()获取当前系统的最优格式(返回值为"rgba8unorm"或"bgra8unorm"之一),再将其赋给format:
// 获取canvas对象constcanvas=document.getElementById("mycanvas");// 获取该canvas的WebGPU渲染上下文constcontext=canvas.getContext("webgpu");// 获取当前系统下最优的纹理像素格式constcanvasFormat=navigator.gpu.getPreferredCanvasFormat();// 使用device与format配置渲染上下文context.configure({device:device,format:canvasFormat,});配置完成后,此后由GPUDevice发起的渲染操作产生的图形,便会显示在这个已关联的画布上。
一个完整的初始化流程
综合以上内容,下面给出一个完整、可直接运行的WebGPU初始化流程示例(仅完成对象创建与画布关联,尚未涉及具体的渲染或计算指令):
asyncfunctioninitWebGPU(){// 检查是否支持WebGPUif(!navigator.gpu){thrownewError("浏览器不支持WebGPU。");}// 获取GPUAdapterconstadapter=awaitnavigator.gpu.requestAdapter();if(!adapter){thrownewError("没有找到GPUAdapter");}// 获取GPU设备constdevice=awaitadapter.requestDevice();if(!device){thrownewError("GPUDevice创建失败");}// 创建指令编码器constencoder=device.createCommandEncoder();if(!encoder){thrownewError("GPUCommandEncoder创建失败");}// 获取canvas对象constcanvas=document.getElementById("canvas_example");if(!canvas){thrownewError("无法获取页面的canvas对象");}// 获取canvas的WebGPU上下文constcontext=canvas.getContext("webgpu");// 获取最佳的像素格式constcanvasFormat=navigator.gpu.getPreferredCanvasFormat();// 配置上下文context.configure({device:device,format:canvasFormat,});}initWebGPU();这就是我们在系列文章的第一篇(WebGPU的因承和编程范式)中定义的初始化函数。至此,我们已经完成了WebGPU应用最基础的初始化环节:确认浏览器与硬件支持能力、建立与GPU的逻辑连接、创建指令编码器,并将渲染目标(canvas)与设备关联起来。在下一篇Part3: 图形渲染中,我们会介绍如何在GPU显存中定义数据,以及如何定义与突然渲染有关的渲染通道。