
异步任务管理:管理“长期运行单页应用”

Motiff 的渲染引擎现已支持 WebGPU,并开始对外灰度。
我们花费了数月时间重构了渲染引擎现最底层的核心代码,让 Motiff 支持 WebGPU,使得渲染引擎能充分利用 GPU 的能力,提升渲染性能。
相较 WebGL,WebGPU 让平均渲染性能提升了 20%,在部分 Intel 集成显卡上性能提升可达 400%。这一提升不仅来自于高效的 API,还得益于我们对渲染架构的调整。
只要你在屏幕上看到的内容在变化,渲染引擎就在工作。渲染引擎的主要工作是将图层数据转换为渲染指令序列,再将渲染指令发送给 GPU,绘制出内容。
最初,Motiff 妙多的渲染引擎基于 Skia 实现,Skia 提供了一套简单易用的接口,为早期开发提供了很多便利,但难以基于业务逻辑做优化,性能不理想。随着 Motiff 的业务迭代,性能成为了瓶颈,我们将渲染引擎迁移到了 WebGL,这是一种更底层的图形 API,能够更直接地与 GPU 交互,大幅提升了渲染性能。
而现在,WebGPU 的引入,为 Motiff 妙多的渲染引擎提供了更强大的支持和优化空间,帮助我们更好地利用硬件的计算能力,进一步提升渲染性能和用户体验。
WebGPU 是 WebGL 的继任者,在为开发者提供更强大、更灵活的图形处理能力的同时,降低了开发难度。
WebGPU 的优势主要体现在以下四个方面:
支持 WebGPU 的第一个问题是我们无法只支持 WebGPU。
由于 WebGPU 在大多数浏览器中仍处于实验性阶段,仅有 Chrome 浏览器在正式版中支持 WebGPU,因此我们无法单纯依赖 WebGPU 来实现渲染引擎,那么同时支持 WebGL 和 WebGPU 成了必然的选项。而在重构前我们的渲染引擎是完全基于 WebGL,难道要写两套渲染引擎吗?业务代码又要如何调用呢?
答案是渲染抽象层。
渲染抽象层屏蔽了底层渲染接口,抹平了 WebGL 和 WebGPU 的差异,提供了一套统一的相对底层的接口。业务不需要判断当前是 WebGL 还是 WebGPU,只关心要画什么三角形,顶底数据存在哪里,将这些作为参数调用渲染抽象层提供的接口。
诚然,WebGL 和 WebGPU 依旧有功能上的差异,在不同硬件不同操作系统不同浏览器上,也有不同的扩展。为了能最大程度上利用新功能提高性能,需要分别针对是否有该功能的两种场景写业务代码。我们设计了一组布尔值来描述当前支持这种功能。值得注意的是,是否支持某种功能与当前的渲染后端是 WebGL 还是 WebGPU 没有任何关联。比如 WebGL 的扩展 WEBGL_blend_func_extended 和 WebGPU 的扩展 dual-source-blending 本质上是同一种功能,可以用同一个布尔值来表达。而计算着色器只有 WebGPU 支持,在 WebGL 下该功能所对应的布尔值总为假。
渲染引擎将渲染抽象层分别用 WebGL 和 WebGPU 实现。我们同时添加了空实现版本,加速无需渲染能力的测试。
渲染抽象层的难点是 WebGPU 和 WebGL 的差异。最容易想到的问题是着色器语言不同。WebGL 使用的着色器语言是 GLSL(OpenGL Shading Language),而 WebGPU 使用的着色器语言是 WGSL(WebGPU Shading Language)。保证一致性的最好方案当然是只写一套代码,WebGL 和 WebGPU 都能用。比如设计一种全新的着色器语言 MTSL(Motiff Shading Language),能分别转化为 GLSL 和 WGSL。但是这个方案的成本有些高,需要搞定语言设计、词法语法分析、目标代码生成以及一系列 IDE 配套设施。
我们使用了更为简单的方案,写两套区别不大的着色器代码。例如渲染纯色,GLSL 代码:
WGSL 代码:
在两种着色器代码中,我们使用了相同的顶点数据结构,相同的 uniform 数据结构,相同的计算过程,从而在代码层面保证渲染结果相同。同时对于每个需要验证渲染结果的测试,我们都分别用 WebGL 和 WebGPU 执行,在测试层面保证渲染结果相同。
其次, WebGPU 和 WebGL 坐标系方向不同。Motiff 妙多的渲染引擎涉及到渲染接口中的三种坐标系,NDC 坐标系(Normalized Device Coordinates)、Framebuffer 坐标系和 Texture 坐标系。这三种坐标系并非业务上的概念,而是底层渲染接口中的概念。
NDC 坐标系可简单视为顶点着色器输出结果所在的坐标系。Framebuffer 坐标系是 viewport,scissor,readPixel 的坐标系。Texture 坐标系一般被称为 UV 坐标系,是片段着色器中纹理采样所使用的坐标系。
WebGL 和 WebGPU 中坐标系的方向并不相同。由于 Motiff 妙多只涉及到 2D 渲染,我们不考虑 z 轴的方向。除此之外,有差异的就是 y 轴。WebGL 中所有坐标系的 y 轴向上,而 WebGPU 中 Framebuffer 和 Viewport 的 y 轴向下。业务中要怎么选择 y 轴的方向呢?
无论何种选择,性能都不应受到影响。而最容易受到影响的功能是纹理数据上传。WebGL 中 Texture 数据上传后是 y 轴翻转的,而 WebGPU 中是正常的。虽然 WebGL 中可以通过 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL)
让 y 轴再次翻转,从而负负得正,让结果正常。
但是这个功能只在 WebGL 中存在,OpenGL 中并没有,而 Motiff 妙多在服务端也有渲染需求,所以无法使用该能力。为了避免对上传 Texture 数据的额外处理,我们选择在 WebGL 中 Texture 内容 y 轴反转,在 WebGPU 中 Texture 内容正常。这个选择也契合了两者 Viewport 坐标系的不同。由于 Framebuffer 中的内容也会被作为 Texture 再次采样,所以渲染出来的内容也需要保证方向和 Texture 一致。
这个选择引入了一个问题,WebGL 中用户看到的内容是 y 轴反转的。原因就是上面说的,渲染出来的内容也需要保证方向和 Texture 一致,在 WebGL 中就是 y 轴反转的。最简单的方案当然是让用户把显示器旋转 180 度,但这个显然不是一个好的方案。除此之外,还有两种方案:
Motiff 选择方案 2,因为这些特殊逻辑可以写到底层 WebGL 对渲染抽象层的实现中。方案 1 必须由业务判断当前的渲染后端是否为 WebGL,而且需要多余的 Framebuffer,在低端移动设备中会是不小的开销。
WebGL 和 WebGPU 虽然存在建模设计上的差异,但在功能上依旧存在大量相同的部分,设计一套兼容各自功能的接口并不复杂。我们在整体上基于 WebGPU 的接口,并参照现有业务逻辑,设计了渲染抽象层的接口。
比如 WebGPU 中有 RenderPipeline 类型,而 WebGL 中是一堆全局变量。在我们的设计中采用了类似 RenderPipeline 的数据结构,存储了单次渲染所依赖的所有状态。
又如 WebGL 中有 Framebuffer 类型,但 WebGPU 中则没有对应的类型,而是在构造 RenderPass 时以参数的方式传入多个 Texture 的组合,实现了和 Framebuffer 一样的功能。在我们的设计中则保留了 Framebuffer 概念,简化在 WebGL 侧的实现。
WebGPU 带来的最大收益,无疑是性能的提升。而 WebGPU 不仅仅是新 API,它是为现代 GPU 设计的 API,它让我们从现代 GPU 角度审视渲染架构设计。如何减少 RenderPassEncoder 数量?如何将 LoadOp 从 Load 转换为 Clear?这些优化是否在任何场景都是正向优化?这些问题为渲染引擎的后续迭代提供了方向。目前的实验数据也已证明这些方向有继续探索的必要。
诚然 WebGPU 还是个新生事物,很多我们期待的功能仍在提案中,比如 push-constants,subgroups。但是它让我们能在 Web 平台下将目光从 30 多年前诞生的接口转向更现代化的 API。