乐趣区

关于webgl:WebGL-与-WebGPU-比对1-前奏

这篇讲讲历史,不太适宜直奔主题的敌人们。

1 为什么是 WebGPU 而不是 WebGL 3.0

你若往 Web 图形技术的底层去深究,肯定能追溯到上个世纪 90 年代提出的 OpenGL 技术,也肯定能看到,WebGL 就是基于 OpenGL ES 做进去的这些信息。OpenGL 在那个显卡羸弱的年代施展了它应有的价值。

显卡驱动

咱们都晓得当初的显卡都要装置显卡驱动程序,通过显卡驱动程序裸露的 API,咱们就能够操作 GPU 实现图形处理器的操作。

问题就是,显卡驱动和一般编程界的汇编一样,底层,不好写,于是各大厂就做了封装 —— 码界的基操。

图形 API 的简略年表

OpenGL 就是干这个的,负责下层接口封装并与上层显卡驱动打交道,然而,家喻户晓,它的设计格调曾经跟不上古代 GPU 的个性了。

Microsoft 为此做进去最新的图形 API 是 Direct3D 12,Apple 为此做进去最新的图形 API 是 Metal,有一个驰名的组织则做进去 Vulkan,这个组织名叫 Khronos。D3D12 当初在发光发热的中央是 Windows 和 PlayStation,Metal 则是 Mac 和 iPhone,Vulkan 你可能在安卓手机评测上见得多。这三个图形 API 被称作三大古代图形 API,与古代显卡(无论是 PC 还是挪动设施)的分割很亲密。

WebGL 能运行在各个浏览器的起因

噢,忘了一提,OpenGL 在 2006 年把丢给了 Khronos 管,当初各个操作系统根本都没怎么装这个很老的图形驱动了。

那问题来了,基于 OpenGL ES 的 WebGL 为什么能跑在各个操作系统的浏览器?

因为 WebGL 再往下曾经能够不是 OpenGL ES 了,在 Windows 上当初是通过 D3D 转译到显卡驱动的,在 macOS 则是 Metal,只不过工夫越靠近当初,这种非亲儿子式的实现就越发艰难。

苹果的 Safari 浏览器最近几年才珊珊反对 WebGL 2.0,而且曾经放弃了 OpenGL ES 中 GPGPU 的个性了,或者看不到 WebGL 2.0 的 GPGPU 在 Safari 上实现了,果子哥当初正忙着 Metal 和更平凡的 M 系列自研芯片呢。

WebGPU 的名称由来

所以,综上所述,下一代的 Web 图形接口不叫 WebGL 3.0 的起因,你分明了吗?曾经不是 GL 一脉的了,为了使古代巨头在名称上不打架,所以采纳了更贴近硬件名称的 WebGPU,WebGPU 从本源上和 WebGL 就不是一个时代的,无论是编码格调还是性能体现上。

题外话,OpenGL 并不是没有学习的价值,反而它还会存在一段时间,WebGL 也一样。

2 与 WebGL 比拟编码格调

WebGL 实际上能够说是 OpenGL 的影子,OpenGL 的格调对 WebGL 的格调影响微小。

学习过 WebGL 接口的敌人都晓得一个货色:gl 变量,精确的说是 WebGLRenderingContext 对象,WebGL 2.0 则是 WebGLRenderingContext2.

OpenGL 的编码格调

无论是操作着色器,还是操作 VBO,亦或者是创立一些 Buffer、Texture 对象,基本上都得通过 gl 变量一条一条函数地走过程,程序是十分考究的,例如,上面是创立两大着色器并执行编译、连贯的代码:

const vertexShaderCode = `
attribute vec4 a_position;
void main() {gl_Position = a_position;}
`

const fragmentShaderCode = `
precision mediump float;
void main() {gl_FragColor = vec4(1, 0, 0.5, 1);
}
`

const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vertexShaderCode)
gl.compileShader(vertexShader)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fragmentShaderCode)
gl.compileShader(fragmentShader)

const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)

// 还须要显式指定你须要用哪个 program
gl.useProgram(program)
// 持续操作顶点数据并触发绘制
// ...

创立着色器、赋予着色器代码并编译的三行 js WebGL 调用,能够说是必须这么写了,顶多 vs 和 fs 的创立编译程序能够换一下,然而又必须在 program 之前实现这些操作。

CPU 负载问题

有人说这无所谓,能够封装成 JavaScript 函数,暗藏这些过程细节,只需传递参数即可。是,这是一个不错的封装,很多 js 库都做过,并且都很实用。

然而,这依然有难以逾越的鸿沟 —— 那就是 OpenGL 自身的问题。

每一次调用 gl.xxx 时,都会实现 CPU 到 GPU 的信号传递,扭转 GPU 的状态,是立刻失效的。相熟计算机根底的敌人应该晓得,计算机外部的工夫和硬件之间的间隔有如许重要,世人花了几十年工夫无不为信号传递付出了致力,上述任意一条 gl 函数扭转 GPU 状态的过程,大抵要走完 CPU ~ 总线 ~ GPU 这么长一段距离。

咱们都晓得,办事必定是一次性备齐资料的好,不要来来回回跑那么多遍,而 OpenGL 就是这样子的。有人说为什么要这样而不是改成一次发送的样子?历史起因,OpenGL 流行那会儿 GPU 的工作没那么简单,也就不须要那么超前的设计。

综上所述,WebGL 是存在 CPU 负载隐患的,是因为 OpenGL 这个状态机制决定的。

古代三大图形 API 可不是这样,它们更偏向于先把货色筹备好,最初提交给 GPU 的就是一个残缺的设计图纸和缓冲数据,GPU 只须要拿着就能够专一办事。

WebGPU 的装配式编码格调

WebGPU 尽管也有一个总管家一样的对象 —— device,类型是 GPUDevice,示意能够操作 GPU 设施的一个高层级形象,它负责创立操作图形运算的各个对象,最初装配成一个叫“CommandBuffer(指令缓冲,GPUCommandBuffer)”的对象并提交给队列,这才实现 CPU 这边的劳动。

所以,device.createXXX 创立过程中的对象时,并不会像 WebGL 一样立刻告诉 GPU 实现状态的扭转,而是在 CPU 端写的代码就从逻辑、类型上确保了待会传递给 GPU 的货色是精确的,并让他们按本人的坑站好位,随时期待提交给 GPU。

在这里,指令缓冲对象具备了残缺的数据资料(几何、纹理、着色器、管线调度逻辑等),GPU 一拿到就晓得该干什么。

// 在异步函数中
const device = await adapter.requestDevice()
const buffer = device.createBuffer({/* 拆卸几何,传递内存中的数据,最终成为 vertexAttribute 和 uniform 等资源 */})
const texture = device.createTexture({/* 拆卸纹理和采样信息 */})

const pipelineLayout = device.createPipelineLayout({/* 创立管线布局,传递绑定组布局对象 */})

/* 创立着色器模块 */
const vertexShaderModule = device.createShaderModule({/* ... */})
const fragmentShaderModule = device.createShaderModule({/* ... */})

/*
计算着色器可能用到的着色器模块
const computeShaderModule = device.createShaderModule({/* ... * /})
*/

const bindGroupLayout = device.createBindGroupLayout({/* 创立绑定组的布局对象 */})

const pipelineLayout = device.createPipelineLayout({/* 传递绑定组布局对象 */})

/*
下面两个布局对象其实能够偷懒不创立,绑定组尽管须要绑定组布局以
告诉对应管线阶段绑定组的资源长啥样,然而绑定组布局是能够由
管线对象通过可编程阶段的代码本人推断进去绑定组布局对象的
本示例代码保留了残缺的过程
*/

const pipeline = device.createRenderPipeline({
  /* 
  创立管线
  指定管线各个阶段所需的素材
  其中有三个阶段能够传递着色器以实现可编程,即顶点、片段、计算 
  每个阶段还能够指定其所须要的数据、信息,例如 buffer 等
  
  除此之外,管线还须要一个管线的布局对象,其内置的绑定组布局对象能够
  让着色器通晓之后在通道中应用的绑定组资源是啥样子的
  */
})

const bindGroup_0 = deivce.createBindGroup({
  /* 
  资源打组,将 buffer 和 texture 归到逻辑上的分组中,不便各个过程调用,过程即管线,此处必须传递绑定组布局对象,能够从管线中推断获取,也能够间接传递绑定组布局对象自身
  */
})

const commandEncoder = device.createCommandEncoder() // 创立指令缓冲编码器对象
const renderPassEncoder = commandEncoder.beginRenderPass() // 启动一个渲染通道编码器
// 也能够启动一个计算通道
// const computePassEncoder = commandEncoder.beginComputePass({/* ... */}) 

/*
以渲染通道为例,应用 renderPassEncoder 实现这个通道内要做什么的程序设置,例如
*/

// 第一道绘制,设置管线 0、绑定组 0、绑定组 1、vbo,并触发绘制
renderPassEncoder.setPipeline(renderPipeline_0)
renderPassEncoder.setBindGroup(0, bindGroup_0)
renderPassEncoder.setBindGroup(1, bindGroup_1)
renderPassEncoder.setVertexBuffer(0, vbo, 0, size)
renderPassEncoder.draw(vertexCount)

// 第二道绘制,设置管线 1、另一个绑定组并触发绘制
renderPassEncoder.setPipeline(renderPipeline_1)
renderPassEncoder.setBindGroup(1, another_bindGroup)
renderPassEncoder.draw(vertexCount)

// 完结通道编码
renderPassEncoder.endPass()

// 最初提交至 queue,也即 commandEncoder 调用 finish 实现编码,返回一个指令缓冲
device.queue.submit([commandEncoder.finish()
])

上述过程是 WebGPU 的一般化代码,很毛糙,没什么细节,不过基本上就是这么个逻辑。

对通道编码器的那局部代码,笔者保留的比拟残缺,让读者更好察看一个指令编码器是如何编码通道,并最初完结编码创立指令缓冲提交给队列的。

厨子戏法

用做菜来比喻,OpenGL 系的编程就好比做一道菜时须要什么调料就去拿什么调料,做好一道菜再持续做下一道菜;而古代图形 API 则是多灶台停火,所有资料都在适合的地位上,包含解决好的食材和辅料,即便一个厨师(CPU)都能够同时做好几道菜,效率很高。

3 多线程与弱小的通用计算(GPGPU)能力

WebWorker 多线程

WebGL 的总管家对象是 gl 变量,它必须依赖 HTML Canvas 元素,也就是说必须由主线程获取,也只能在主线程调度 GPU 状态,WebWorker 技术的多线程能力只能解决数据,比拟鸡肋。

WebGPU 扭转了总管家对象的获取形式,adapter 对象所依赖的 navigator.gpu 对象在 WebWorker 中也能够拜访,所以在 Worker 中也能够创立 device,也能够拆卸出指令缓冲,从而实现多线程提交指令缓冲,实现 CPU 端多线程调度 GPU 的能力。

通用计算(GPGPU)

如果说 WebWorker 是 CPU 端的多线程,那么 GPU 自身的多线程也要用上。

能实现这一点的,是一个叫做“计算着色器”的货色,它是可编程管线中的一个可编程阶段,在 OpenGL 中堪称是捷足先登(因为晚期的显卡并没开掘其并行通用计算的能力),更别说 WebGL 到了 2.0 才反对了,苹果老兄甚至压根就懒得给 WebGL 2.0 实现这个个性。

WebGPU 出厂就带这玩意儿,通过计算着色器,应用 GPU 中 CU(Compute Unit,计算单元)旁边的共享内存,速度比一般的显存速度快得多。

无关计算着色器的材料不是特地多,目前只能看例子,在参考资料中也附带了一篇博客。

将 GPGPU 带入 Web 端后,脚本语言的运行时(deno、浏览器 JavaScript,甚至将来的 nodejs 也有可能反对 WebGPU)就能够拜访 GPU 的弱小并行计算能力,据说 tensorflow.js 改用 WebGPU 作为后置技术后性能有极为显著的晋升,对深度学习等畛域有极大帮忙, 即便用户的浏览器没那么新潮,渲染编程还没那么快换掉 WebGL,WebGPU 的通用计算能力也能够在别的畛域发光发热,更别说计算着色器在渲染中也是能够用的。

真是迷人啊!

4 浏览器的实现

Edge 和 Chrome 截至发文,在金丝雀版本均能够通过 flag 关上试用。

Edge 和 Chrome 均应用了 Chromium 外围,Chromium 是通过 Dawn 这个模块实现的 WebGPU API,依据无关材料,Dawn 中的 DawnNative 局部负责与三大图形 API 沟通,向上则给一个叫 DawnWire 的模块传递信息,DawnWire 模块则负责与 JavaScript API 沟通,也就是你写的 WebGPU 代码。WGSL 也是这个局部实现的。Dawn 是 C++ 实现的,你能够在参考资料中找到连贯。

FireFox 则应用了 gfx-rs 我的项目实现 WebGPU,显然是 Rust 语言实现的 WebGPU,也有与 Dawn 相似的模块设计。

Safari 则更新自家的 WebKit 实现 WebGPU。

5 将来

瞻望宏图之类的话不说,然而随着红绿蓝三家的 GPU 技术越发精湛,加上各个挪动端的 GPU 逐步起色,三大古代图形 API 必定还在倒退,WebGPU 肯定能在 Web 端开释古代图形处理器(GPU)的弱小能力,无论是图形游戏,亦或是通用并行计算带来的机器学习、AI 能力。

参考资料

  • Google Dawn Page
  • gfx-rs GitHub Home Page
  • Get started with GPU Compute on the web
退出移动版