Part 01
WebGPU研发背景
早期,在使用GPU模块开发Web应用方面,开发者更多的是使用2011年发布的WebGL API进行图形绘制。这套API基于OpenGL ES,在一段时间内是Web端进行底层GPU图形绘制的唯一选择,可编程GPU语言的加入,让它在从事某些绘制工作的性能方面对Canvas2D保持一定的优势。该API通过canvas元素获取WebGL上下文后才能使用,其以内部全局状态为中心而设计的状态机式的API调用深受开发人员的诟病,开发人员必须小心构建API的调用顺序(过程式调用),管理状态的开启以及恢复,以使绘制结果正确,同时这在一定程度上导致了性能的开销。
随着科技的发展,GPU早已不是图形绘制应用的专属,在元宇宙、机器学习、大数据、神经网络等不同领域大放异彩,随着算力需求的日益提升,GPU的作用愈发重要,与此同时,在桌面端出现了新一代的图形API(Vulkan、Metal、DirectX12),它们采用面向对象的设计方案,为开发人员提供更加底层的接口访问,更多的GPU使用权,灵活的API调用方式以及通用并行计算能力,让开发人员最大限度从GPU中榨取性能。
Web端同样需要这些能力,基于现代图形API的设计理念,WebGPU应运而生,它不是WebGL的一次升级,WebGPU拥有自身独特的抽象设计,并不直接封装某一特定的图形API,以下是WebGPU的架构示意图。
Part 02
WebGPU中的重要概念
2.1适配器和设备
在开始了解WebGPU的相关规范时,最先接触的便是适配器(adapter)和设备(device)的概念,下图展示了从物理设备(GPU)到逻辑设备抽象架构。
适配器,即GPUAdapter。一个物理GPU设备对应一个GPUAdapter,计算机可能具有多个GPU设备(集成显卡和独立显卡),适配器作为翻译者角色,链接WebGPU与本机的图形API。通过下述方式可以获取相应的GPUAdapter。
这里的设备,即GPUDevice,是逻辑设备的概念,并不对应真正的GPU。GPU是共享资源,浏览器可以运行多个Web应用,每个Web应用都可以独立使用GPU,需要一个类似代理人角色,帮助多个独立的Web应用使用GPU相关的功能,这便是WebGPU设备的作用,GPUDevice对象是后续使用相关API的重要对象,从某种意义上它很像WebGL的上下文概念,但它并不与canvas强相关。通过下述方式获取GPUDevice。
2.2 着色器
着色器(shader)是运行在GPU的一段程序。现代GPU渲染是通过流水管线(可编程逻辑管线)的方式实现的,在管线执行的某个阶段(可编程部分)会执行着色器代码。如果你了解过WebGL,可能知道顶点着色器(vertex shader)和片段着色器(fragment shader),应用程序组织数据资源以变量(unifrom/attribute)的形式传递给着色器,着色器运行将执行的结果传递给下一个阶段进行处理。
着色器是开发人员操控GPU的重要工具,复杂计算、场景特效、图像处理等均可交给着色器程序处理。WebGPU不仅含有顶点着色器和片段着色器、同时具备执行通用并行计算的能力,即计算着色器(compute shader),它由WebGPU计算管线(下文管线概念介绍)承载,拥有比WebGL更强大的计算能力。WebGL采用GLSL语言(OpenGL采用的语言)实现着色器代码,而WebGPU拥有重新设计的着色器语言WGSL,下面是着色器代码与对应模块(GPUShaderModule)的创建示例。
2.3 资源(缓冲、纹理、采样器)
上述着色器的示例中,定义了一些变量,例如unfiorms、uTexture、uSampler、aPosition、aUv等,这些变量参数的值即对应外部应用程序的数据资源,这些数据会存储在显存中,最终会被传入到着色器程序中运行以得到相应的结果。数据资源大体可分为四类:顶点属性数据(vertex attribute)、着色器变量(uniform buffer)数据、纹理数据(texture)、采样器(sampler)。
顶点属性数据主要存储顶点的位置坐标、法向量、纹理坐标(用于采样纹理)等,是基本绘制所必须的。着色器变量数据,则是着色器程序运行所需的通用数据,例如仿射变换矩阵、场景光照参数、材质参数等。纹理数据更多的用于存储图像资源,在绘制时常用于贴图效果的实现。采样器则是一种特殊资源,它指定纹理编码和滤波需要的方式,例如纹理的放大与缩小,各向异性滤波,minmap生成等。对于顶点属性数据和着色器变量数据,其主要映射到GPUBuffer中,即顶点缓冲对象(VBO)和uniform缓冲对象(UBO),纹理数据则对应GPUTexture,采样器则是GPUSampler对象。这三种类型的资源均由GPUDevice创建。以下是各类型资源创建的示例。
GPUBuffer的创建采用了缓冲映射(Buffer Mapping)机制,当某个显存被映射了,CPU才能访问它。上述例子中,在创建GPUBuffer时将mappedAtCreation设置为true,开启映射机制,在设置完数据后结束映射。
2.4 绑定组
上述示例中分别创建了用于存储顶点属性的GPUBuffer对象,存储uniform变量的GPUBuffer对象,存储图像资源的GPUTexutre对象以及采样器对象。对于顶点属性的GPUBuffer对象,将在后续的管线与命令编码模块中阐述其是如何传入到GPU中。对于后述的3种资源(着色器变量、纹理、采样器),则需要用一种有效的方式将它们提交给GPU,为此,WebGPU提出了绑定组概念,即GPUBindGroup,它是一种数据容器,用于将部分数据资源进行打组并传递给着色器程序,能高效地进行数据组织与分配。通过打组的数据组织形式,能够减少CPU与GPU通讯次数,从而提高性能,同时也方便不同行为的着色器共享相同的打组资源,实现资源的复用。下图给出了WebGL与WebGPU不同的数据组织传递形式。
从上图可以看出,WebGL的API设计是围绕内部的全局状态设定的实现的,通过API函数逐个将资源绑定到绑定点上,本质上更改了内部全局状态,而WebGPU则是将资源数据放入数据容器中,通过命令提交(编码器与队列介绍)的方式送入到GPU中。创建GPUBindGroup需要对应的描述符,其结构如下。
绑定组有对应的布局(GPUBindGroupLayout),布局向着色器程序描述某个资源的类型(type),所属组(group),对应的绑定点位(bingding)以及用于具体阶段的着色器程序(visibility),仔细观察上述着色器部分里给出的示例,你会发现@group(0) @binding(0)这样的声明,即表示该资源绑定在组0的0号绑定点上,绑定的布局需根据着色器程序中的设置进行对应填写。GPUBindGroupEntry对象表明一个绑定位,在这个绑定位(resouce字段上指定)上会附上WebGPU创建的资源数据。以下是一个GPUBindGroup创建的简单示例,我们将之前创建的GPUBuffer对象、采样器与纹理对象打包到一个绑定组对象。
2.5 管线
完成着色器模块创建和数据资源准备之后,还需要进行一项重要的工作,即管线(Pipeline)的搭建。大多数开发者在开始学习图形渲染时,首先接触的便是渲染管线的概念,这是现代图像渲染的重要机制,但在WebGL API设计中却没有体现出这一重要理念,零碎的API组织形式让初学者很难将每一步与GPU管线联系起来,WebGL要求开发人员自行组织应用程序的执行流程,所以你会看到gl.bindVertexArray、gl.bindBuffer、gl.bindTexture、gl.useProgram这样的API设计,按照不同需求绑定不同的资源或状态,从而实现不同物体或效果的绘制。WebGPU中的管线分为渲染管线和计算管线。
渲染管线(GPURenderPipeline)顾名思义是用于绘制的管线,通过该管线的作用,最终会生成一副2D图像,该图像可以在屏幕上展示,也可以渲染到帧缓冲区中(frame buffer)。创建GPURenderPipeline需要对应的描述符,其结构如下。
GPUVertexState与GPUFragmentState字段分别代表了顶点着色器和片段着色器可编程阶段。GPUPrimitiveState用于指定图元装配形式,在进行光栅化时以何种图元类型进行绘制。GPUDepthStencilState用于描述深度模版测试信息。GPUMultisampleState指定多重采样,用于处理锯齿效果。GPURenderPipeline创建示例如下。
上述示例可以看出,在渲染管线中配置上了之前生成的两个着色器模块,同时也描述了顶点属性(资源部分提到)在着色器中的布局。在顶点着色器中,有@location(0) aPosition与@location(1) aUv这两个定义,分别代表传入的顶点的位置属性和uv坐标属性,location(0)和location(1)是与管线配置你中的shaderLocation相对应的。
WebGL在大多数情况下仅是一套图形绘制API,它很少会被用来进行其他事物处理,比如计算。计算管线(Compute Pipeline)的出现则赋予了WebGPU“计算能力”,它不是传统渲染管线的一部分,用于GPU并行计算,生成的最终结果存储于缓冲区中,该缓冲区可以存储任何类型的数据,计算管线只有一个compute阶段,创建GPUComputePipeline需要对应的描述符,其结构如下。
GPUProgrammableStage表明这是一个可编程的阶段,类似与GPUVertexState和GPUFragmentState。每个顶点的处理需要调用一次顶点着色器,片段着色器会执行每个像素的处理,而计算着色器则根据开发人员定义的工作项(work item)进行调用,每个工作项对应一个线程(thread)。工作项的集合被分为工作组(work group),即是一组线程(thread block),这组线程内可以共享内存、相互通信及协调运算。在WebGPU中,工作组被模拟为三维网格,如下图所示。
每个最小立方块(黑边)可以看作是一个工作项,多个工作项集合成工作组(红虚边)。在计算着色器代码中可以看到@workgroup_size(x, y, z)这样的申明,即是告诉GPU这个计算着色器的工作组是多大,工作组尺寸(workdgroup_size)的设置大多数情况下取决于工作项坐标语义。下图为简单的GPUComputePipeline创建示例。
这是一个简单的图像灰度直方图统计示例。通过GPU并行架构处理,我们能够忽略掉图像像素的遍历统计,极大加快计算的速度。
2.6 命令编码和队列
上述的工作可以看作是准备阶段,主要进行数据准备和管线搭建两项工作,在进行最后的绘制或计算时,则需要通过命令和队列的形式实现。命令编码器(GPUCommandEncoder)主要常用功能有两个:创建通道编码器(pass encoder)和缓冲资源(GPUBuffer/GPUTexture)复制。GPUCommandEncoder由设备对象形创建,如下:
WebGPU的通道分为渲染通道(render pass)和计算通道(compute pass),对应渲染管线和计算管线,两类通道对象分别通过GPUCommandEncoder对象上的相应方法(beginRenderPass/beginComputePass)结合自身描述符实现创建与启动,最终会得到通道编码器对象GPURenderPassEncoder/GPUComputePassEncoder,这类编码器是WebGPU API设计中的抽象概念,也是WebGL全局状态设置的替代品。通过编码器对象可以设置需要的管线、绑定组、顶点属性缓冲并调用draw/dispatch函数进行绘制或计算。下面是编码器对象的使用示例。
GPUCommandEncoder对象在调用finish函数后会得到一个命令缓冲区对象(GPUCommandBuffer),该缓冲区用于存储GPU命令,这些命令的提交则是通过命令队列(GPUQueue)的实施的,如下:
Part 03
结束语
WebGPU作为全新的API,为Web应用开发注入了新的活力,它实现了图形绘制到通用并行计算的进步,让GPU成为Web端应用的重要角色,是未来构建高性能应用的关键。