上帝视角看 GPU 学习笔记
上帝视角看GPU(1):图形流水线基础_哔哩哔哩_bilibili
《上帝视角看 GPU》龚大教程学习笔记。
一、图形流水线基础
首先思考一张图片是如何显示在屏幕上呢?首先需要了解一个概念帧缓存(Frame Buffer),这是内存中的一块区域,这块区域上的内容和显示器上显示的像素是一一对应的。
将帧缓存上的内容输出到屏幕上,需要通过显卡,显卡上有个输出端口连接到显示器上。
那这是最简单的需求,如果此时有一个需求:需要将图像的亮度提升 2 倍呢?也就是将每个像素的 RGB 分量都乘 2。
当然可以通过 CPU 来进行计算,但这样必然要占用大量 CPU 资源,更好的做法是加入一个 处理器(PU),这个处理器可以在每个像素上执行同样的操作;
为了适应更加灵活多变的需求,比如将图像上半部分亮度提升 2 倍,下半部分亮度提升 4 倍。我们可以在 PU 上挂上一个 **程序 **,这个程序是单入单出的,输入是像素坐标,输出的是像素 RGB 颜色,也就是 片元着色器 Pixel Shader。
好了,上边就是一个最基本的针对图像的流水线。
顺便一说,在任天堂的红白机上,就有这么一个处理器,叫做 PPU(Picture Processing Unit)
那么对于显示一个三维模型呢?我们来看看一个基础的完整的图形渲染管线:下图是一个最基础的图形渲染管线,其中绿色的部分是可编程的阶段,包括顶点着色器、片元着色器,红色的部分是固定流水线单元(Fixed-pipeline Unit),为了效率由硬件直接实现。
阶段 1:Input Assembler
Input Assembler 输入装配器,是图形流水线的第一个**固定阶段**(不可编程,但可配置)。它直接与应用程序(CPU端)提交的数据打交道。💡PS:Input Assembler 是 Direct3D 中的明确标识的管线阶段,用于组装从 Vertex Buffers 和 Index Buffers 中的顶点数据,形成图元。
但在 OpenGL 中没有对应的阶段,这一“组装”的工作需要手动处理,通过
glVertexAttribPointer
系列函数来指定顶点属性的格式和布局。
阶段 2: Vertex Shader
顶点着色器(Vertex Shader)是 图形渲染管线中第一个可编程的 部分,图形渲染管线的第一个可编程部分是顶点着色器(Vertex Shader),它把一个单独的顶点 Vertex 作为输入,经过 shader 处理后输出,顶点着色器主要的目的是对 3D 坐标进行 MVP 变换,变换为屏幕坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
阶段 3:Primitive Assembler
图元装配(Primitive Assembly)阶段也是一个固定阶段,用于将顶点着色器(或几何着色器)输出的所有顶点作为输入(如果是GL_TRIANGLE,那么就是一个三角形),并将所有的点装配成指定图元的形状。
阶段 4:Rasterizer
图元装配阶段的输出会被传入光栅化阶段(Rasterization Stage),光栅化其实就是找出三角形所覆盖区域对应的屏幕上的像素,从而将这些像素提供给片元着色器。光栅化阶段也是一个固定流水线单元,是一个算法固定的操作,由硬件直接处理,不可编程。
在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出屏幕范围以外的所有像素,用来提升执行效率。
💡 注意光栅化本质上是将顶点信息插值到图源覆盖的每个像素上。
这也可以解释从顶点着色器传值给片元着色器的值,会经过插值。这其实就是光栅化阶段后,片元着色器接收到的已经不是某个顶点的原始输出数据了,而是经过光栅化器插值后的、针对当前这个特定片元的值,这些变量在片元着色器中声明为
in
变量。
阶段 5: Pixel Shader
片段着色器 Pixel Shader 的主要目的是计算一个像素的最终颜色,这也是所有高级效果产生的地方。跟顶点着色器 Vertex Shader 一样,Pixel Shader 也是一个单入单出的结构,#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
阶段 6:Output Merger
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做**Output Merger**(或者Alpha测试和混合)阶段,这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
Output Merger 也是固定流水线单元;
总结
+ 整个图形流水线要经过几个阶段,其中 Vertex Shader 和 Pixel Shader 是我们可以进行编程的阶段。 + 我们也必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。 + Vertex Shader 和 Pixel Shader 编写就像是在编写一个回调函数,他们会对每一个顶点、每一个像素上执行。二、逻辑上的模块划分
上一部分我们介绍了最基本的图形流水线,随着时代的发展,新的需求逐渐出现,现代的 GPU 不仅仅用于图形渲染,还可以进行通用计算、用于神经网络训练的 TensorCore 、用于视频编解码等多个领域,这些不同的领域在 GPU 上都是独立的模块,具有独立的流水线。我们来看看现在的 GPU 流水线是什么样的,以及他是如何发展来的。1、 图形
Geometry Shader
Vertex Shader 和 Pixel Shader 都是单入单出的结构,只能处理单个顶点和像素。但如果我们要处理的是一个图元(三角形),就没法处理了。这个需求催生了几何着色器 Geometry Shader 的出现, Geometry Shader 是渲染管线中一个可选的可编程阶段,它位于顶点着色器之后、图元装配和光栅化之前。几何着色器的输入是一个完整的图元及其所有顶点数据(例如,一个三角形需要输入3个顶点),而它的输出是零个、一个或多个全新的图元。
换句话说,它是单入多出的结构。
注:Geometry Shader 看起来非常灵活,但实际使用时往往会发现性能很差,这是因为如由于灵活,硬件无法做各种假设来提升性能,只能实现的非常保守。
tessellation
tessellation 的出现是因为对三角形细分需求的逐步增加,由于使用 Geometry shader 性能较差,GPU 流水线在 Vertex shader 后增加了专门的tessellation 功能,他是由三个部分组成 Hull Shader、tessellation、Domain Shader。
2、 计算
GPGPU 通用 GPU
由于 GPU 在图形渲染领域强大的计算能力,逐渐出现了使用 GPU 进行其他领域更加通用的并行计算的想法。最早的想法是渲染一个覆盖屏幕的大三角形,在 Pixel Shader 里做通用的并行计算,相当于每一个像素 Pixel 是一个线程, 这种方式虽然很“hack”,但也产出了很多的成果,这个方向就是** GPU 通用计算(GPGPU)**。
这种方式虽然解决了一些问题,但也存在学习成本高(开发人员需要学习整个图形流水线)、存在性能浪费(还是需要通过整个图形流水线包括顶点着色器)。
这个需求进一步催生了有硬件支持的 GPGPU,不需要再通过图形流水线中那些固定的阶段,同时支持“多入多出”,独立与图形流水线单独存在,应用 GPU 上的计算单元进行通用计算的 shader 叫做 compute shader
。
3、 光线追踪
随着游戏对画面真实感的要求越来越高,基础图形流水线采用光栅化渲染方式,对于实现高质量画面往往要采用很多的 Hack,光线跟踪这一古老的技术逐渐引起了人们注意。由于光线跟踪与光栅化渲染方式有着完全不一样的流程,长期以来研究人员一直在研究如何利用先有的 GPU 硬件实现光线跟踪,这样的需求随着 GPU 在硬件层面提供光线跟踪支持得到了实质的发展。
三、部署到硬件
前边介绍了 GPU 在逻辑模块上的划分,本节我们来看下具体对应到硬件上,GPU 是如何设计的。unified shader
最开始的 GPU 设计上,Vertex Shader 和 Pixel Shader 有对应的处理单元进行处理,比如 2003 年的 GeForce FX 5600,有 2 个 Vertex Shader 单元和 4 个 Pixel Shader 单元,这意味着当顶点和像素的工作量是 1:2 的时候,他们才能发挥出最高效率。对于如果有一堆很小的三角形挤到一起(顶点多像素少)或很大的三角形覆盖(顶点少像素多)的情形都不能发挥出很高的效率。最初这样设计是因为人们通常只用 Vertex Shader 处理坐标数据,需要有较高的精度处理能力;而 Pixel Shader 只处理纹理,只需低精度运算器,但需要采样器。
而随着需求的发展,Vertex Shader 和 Pixel Shader 的能力界限逐渐变的模糊。大规模地形渲染的需求,使得 Vertex Shader 得能读取纹理,而 Pixel Shader 进行通用计算的需求,也使得 Pixel Shader 得能处理高精度数据,最终就是两种处理单元统一了起来,叫做 <font style="color:rgb(15, 17, 21);">unified shader</font>
,这样 GPU 也因为一致性而变得简单。
在 GPU 工作时,由调度器根据工作任务进行动态分配,决定哪些<font style="color:rgb(15, 17, 21);"> unified shader</font>
用于处理 Pixel Shader,哪些处理 Vertex Shader。
最终的结果就是虽然图形流水线中有那么多的 shader,但在硬件层面他们的执行单元都是一样的。
四、完整的软件栈
理想的软件分层体系
理想的关于 GPU 软件分层体系:-
API
应用程序接口层:为应用程序提供统一的编程接口(如 OpenGL、Vulkan、DirectX、CUDA 等),开发者使用 API 编写图形或计算任务,无需直接处理底层硬件细节; -
OS
操作系统层; -
DDI
设备驱动程序接口:是操作系统与 GPU 驱动程序之间的标准接口。由操作系统定义和实现; -
Driver
驱动程序:将 API 调用翻译成 GPU 能理解的指令,管理 GPU 资源(如显存、命令队列),由 GPU 硬件厂商(如NVIDIA、AMD)实现,并且必须严格遵循DDI的规范; - GPU:GPU 接收由驱动程序提交的命令和数据,进行并行处理
但现实情况下会不一样,操作系统就包括了用户态和内核态。
Direct 3D
第一个例子是微软的 Direct 3D(D3D),这个 API 不跨平台(windows),但跨厂商(如NVIDIA、AMD)。它将驱动拆分为了(用户态UMD + 内核态KMD),并引入引入核心调度器(DXGK),将厂商实现驱动程序由作文题变为填空题,减少了驱动程序开发工作量。
Direct 3D 采用自顶向下的模式,由微软定义 API,厂商来进行实现,不方便进行扩展。在 GPU 拥有新功能的硬件支持后,只有等待 Direct 3D 发布了新版本才能支持。
另外 D3D 的 每个版本的 API 是不兼容的,这意味着每出一版 D3D,程序都得大改才能用上。
OpenGL
OpenGL 是跨平台且跨厂商的,由 Khronos (开源组织)发布。在不同的操作系统上,OpenGL
在 Windows 上,Windows 只提供了一个框架可安装用户驱动 ICD,让硬件厂商来实现 OpenGL runtime 的 UMD。
在 Linux 上,有两种方式,一种是完全由厂商来实现 UMD 和 KMD;另一种是基于 Mesa 框架。
OpenGL 的 API 设计是向下兼容的,之前的代码往往新版本也能用。