Chapter2 The Graphics Processing Unit 图形处理器
2.1 Data-Parallel Architectures 数据并行体系结构
2.1.1为什么要了解数据并行体系结构
所有GPU都实现了这些架构的理念,从而导致系统具有严格的限制,但单位功耗达到的计算能力却更高。了解该系统的运行方式有利于更有效的利用它提供的功能。
2.1.2什么是数据并行体系结构
不同处理器使用不同策略避免停顿。CPU经过优化,可以处理各种数据结构和大型代码库。CPU有多个处理器,但都以串联方式运行代码。为了最大程度的减少延迟影响,CPU的大部分芯片由快速本地缓存组成,内存中充满了下一步可能需要的数据。CPU还通过诸如分支预测,寄存器重命名和高速缓存预取等技巧来避免卡顿。GPU的大部分芯片面积属于一大组处理器,其被称为shader cores(着色核心),通常有数千个。
GPU是一个流处理器,其中相似数据的有序集合被依次处理(例如一组顶点,像素),GPU能大规模并行处理这些数据。另一个要点是这些调用要尽可能的独立,这样就不需要调用相邻的信息,也不共享内存位置。当然有时也会例外,以潜在延迟作为代价(一个处理器需要等待另一个处理器的结果)。GPU针对吞吐量做了优化,其被定义为可以处理数据的最大速率。然而代价就是专用于高速缓冲存储器核心的延迟通常比CPU高得多。
假设一个三角面有两千个像素片段需要光栅化处理,像素着色器要被调用两千次。如果使用只有一个着色处理器的GPU,现在开始处理第一个片段。首先着色处理器对寄存器中的值进行一些运算,寄存器是本地的,不会影响速度。下一步处理一条指令,例如访问纹理。纹理是一个完全独立的资源,不是像素程序的本地内存。一次内存读取可能需要数百到数千个时钟周期,在此期间GPU只能等着,像素处理器也得等返回的颜色值。
为了改进这个GPU,给每个片段一个小的存储空间来存放它的本地寄存器。现在,着色处理器不再在纹理获取上停滞,而是切换并执行另一个片段(2000个中的第2个)。与处理第一个时相同,执行一些函数,然后再次遇到纹理读取。如此反复处理完所有片段。此时着色器回到第一个片段。此时纹理颜色已被获取并可用,因此着色程序可以继续执行。处理器以类似的方式运行,直到一个已知的暂停指令或完成整个处理。
此架构中,通过切换到另一个片段,让GPU保持繁忙,从而隐藏延迟。GPU通过将指令将逻辑与数据分离,使这种延迟隐藏的设计更进一步。其称为单指令多数据流(SIMD),这种操作在固定数量的着色器程序上以锁步方式执行相同命令。SIMD优势在于,与使用单独逻辑和调度单元来运行每个程序的处理器相比,SIMD处理数据所需的芯片面积和功耗都更少。将两千个片段放入现代GPU,片段调用像素着色器称为线程(thread)。和CPU线程不同,它包含一些着色器输入值,以及着色器执行所需的任何寄存器空间。使用相同着色器程序的线程被捆绑成组(NVIDIA:warps,AMD:wavefronts).warps/wavefronts安排GPU内核来执行,从8到94,使用SIMD处理,每个线程都映射到一个SIMD通道。
假设有2000个线程要执行。NVIDIA GPU 上的warps包含了32个线程。这产生2000/32=62.5个warps,也就是分配了63个warps。warps的执行过程类似单个GPU处理实例(上面提到的)。着色器程序在所有32个处理器上以锁步方式执行。当遇到内存获取时,所有线程都会同时遇到,因为所有线程执行相同指令。fetch表示这个warp线程将停止,其它线程等待它们返回的结果。不是停滞,warp替换成不同的包含32个线程的warp并由32个核心执行。这种替换很快,因为在换入换出warp时不会触及线程中的数据。替换新的warp只是将一组内核指向一组不同的线程来执行;没有其它开销。warp 执行或替换,直到完成所有。
在这个简单的示例中,纹理的获取延迟可能会导致warp被替换。然而还有比替换warp更短的延迟,因为替换的成本非常低。还有其它几种技术用于优化执行,但替换warp是所有GPU最主要的延迟隐藏机制。此过程的工作效率设计几个因素。例如,如果线程很少,则会创建很少的warp,从而使延迟隐藏成为问题。
着色器代码的结构是影响效率的重要表征。一个主要原因是每个线程寄存器的用量。在我们的示例中,假设2000个线程可以同时驻留在GPU上。与每个线程相关联的着色程序所需的寄存器越多,GPU中可以驻留的线程就越少,因此warp也就越少。缺少warp意味着无法通过替换的方法来缓解失速。常驻的warp被称为“in flight”,其数量被称为occupancy(占用率)。高占用率意味着有许多warp可用于处理,因此空闲处理器更可能比较少。低占用率通常会导致性能不佳。内存获取的频率也会影响需要多少延迟隐藏。
另一个影响整体效率的因素是由“if”语句和循环引起的动态分支。假设在着色程序中遇到if语句。且有某些线程采用备用路径,则warp必需执行两个分支,丢弃每个特定线程不需要的结果。这个问题被称为thread divergence (线程发散),其中一些线程可能需要执行循环迭代warp中其它线程不需要的if路径,从而使它们在此期间处于空闲状态。
2.2 GPU Pipeline Overview管线概述
上图中,不同颜色的阶段表示了该阶段不同属性。其中:
- 绿色的阶段都是完全可编程的。
- 黄色的阶段可配置,但不可编程。
- 蓝色的阶段完全固定。
2.2.1概览
1.顶点着色器(The Vertex Shader)
完全可编程的阶段,顶点着色器可以对每个顶点进行诸如变换和变形在内的很多操作,提供了修改/创建/忽略顶点相关属性的功能,这些顶点属性包括颜色、法线、纹理坐标和位置。顶点着色器的必须完成的任务是将顶点从模型空间转换到齐次裁剪空间。
2.几何着色器(The Geometry Shader)
位于顶点着色器之后,允许 GPU 高效地创建和销毁几何图元。几何着色器是可选的,完全可编程的阶段,主要对图元(点、线、三角形)的顶点进行操作。几何着色器接收顶点着色器的输出作为输入,通过高效的几何运算,将数据输出,数据随后经过几何阶段和光栅化阶段的其他处理后,会发送给片段着色器。
3.裁剪(Clipping)
属于可配置的功能阶段,在此阶段可选运行的裁剪方式,以及添加自定义的裁剪面。
4. 屏幕映射(Screen Mapping)三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)
阶段是固定功能阶段。
5. 像素着色器(Pixel Shader,Direct3D 中的叫法)
常常又称为片断着色器,片元着色器(FragmentShader,OpenGL 中的叫法),是完全可编程的阶段,主要作用是进行像素的处理,让复杂的着色方程在每一个像素上执行。
6. 合并阶段(The Merger Stage)
处于完全可编程和固定功能之间,尽管不能编程,但是高度可配置,可以进行一系列的操作。其除了进行合并操作,还分管颜色修改(Color Modifying),Z 缓冲(Z-buffer),混合(Blend),模板(Stencil)和相关缓存的处理。
2.2.3 可编程着色模型 The Programmable Shader Stage
现代着色阶段,使用了通用着色核心(commone shader core),这就表明顶点,片段,几何着色器共享一套编程模型
模型着色语言都是C-like的语言,比如HLSL,CG,GLSL,其被编译成独立于机器的汇编语言语言,也称为中间语言(IL)这些汇编语言在单独的阶段,通常是在驱动中,被转化成实际的机器语言。这样的安排可以兼容不同的硬件实现。这些汇编语言可以被看做是定义一个作为着色语言编译器的虚拟机。这个虚拟机是一个处理多种类型寄存器和数据源、预编了一系列指令的处理器。
着色语言虚拟机可以理解为一个处理多种类型寄存器和数据源、预编了一系列指令的处理器。考虑到很多图形操作都使用短矢量(最高四位),处理器拥有 4 路 SIMD(single-instruction multiple-data,单指令多数据)兼容性。每个寄存器包含四个独立的值
2.2.3.1 着色模型对比 Comparison of Shader Models
(有个印象就好)
2.2.4 顶点着色器 Vertex Shader
麻麻捏,这个可是重量级_(:з)∠)_,着色部分会单独开一章来讲,杂糅在一起反而看起来会非常乱
顶点着色器是完全可编程的阶段,是专门处理传入的顶点信息的着色器,顶点着色器可以对每个顶点进行诸如变换和变形在内的很多操作。顶点着色器一般不处理附加信息,也就是说,顶点着色器提供了修改,创建,或者忽略与每个多边形顶点相关的值的方式
顶点着色器的输出可以以许多不同的方式来使用,通常是随后用于每个实例三角形的生成和光栅化,然后各个像素片段被发送到像素着色器,以便继续处理
2.2.5 几何着色器 The Geometry Shader
几何着色器(Geometry Shader)是顶点和片段着色器之间一个可选的着色器
几何着色器的输入是单个对象及对象相关的顶点,而对象通常是网格中的三角形,线段或简单的点。另外,扩展的图元可以由几何着色器定义和处理
-
几何着色器可以改变新传递进来的图元的拓扑结构,且几何着色器可以接收任何拓扑类型的图元,但是只能输出点、折线(line strip)和三角形条(triangle strips)
-
几何着色器需要图元作为输入,在处理过程中他可以将这个图元整个丢弃或者输出一个或更多的图元,这个能力被叫做几何增长,
当我们未添加几何着色器时,默认的行为是将输入的三角形直接输出,当我们添加机核着色器之后,就可以修改输出的图形了
2.2.5.1 流输出 Stream Output
GPU 的管线的标准使用方式是发送数据到顶点着色器,然后对所得到的三角形进行光栅化处理,并在像素着色器中处理它们
在顶点着色器(以及可选的几何着色器中)处理顶点之后,除了将数据发送到光栅化阶段之外,也可以输出到流,也就是一个有序数组中进行处理。事实上,可以完全关掉光栅化,然后管线纯粹作为非图形流处理器来使用。以这种方式处理的数据可以通过管线回传
这种操作特别适用于模拟流动的水或其他粒子特效。
2.2.6 像素着色器 Pixel Shader
像素着色器(Pixel Shader,Direct3D 中的叫法),常常又称为片断着色器,片元着色器(Fragment Shader,OpenGL 中的叫法),用于进行逐像素计算颜色的操作,让复杂的着色方程在每一个像素上执行。
在顶点和几何着色器执行完其操作之后,图元会被裁剪、屏幕映射,结束几何阶段,到达光栅化阶段,在光栅化阶段中先经历三角形设定和三角形遍历,之后来到像素着色阶段
像素着色器常用来处理场景光照和与之相关的效果,如凸凹纹理映射和调色。名称片断着色器似乎更为准确,因为对于着色器的调用和屏幕上像素的显示并非一一对应
需要注意,像素着色程序通常在最终合并阶段设置片段颜色以进行合并,而深度值也可以由像素着色器修改。模板缓冲(stencil buffer)值是不可修改的,而是将其传递到合并阶段(Merge Stage)
可以发现,顶点着色程序的输出,在经历裁剪、屏幕映射、三角形设定、三角形遍历后,实际上变成了像素着色程序的输入。
2.2.7合并阶段 The Merging Stage
作为光栅化阶段名义上的最后一个阶段,合并阶段(The Merging Stage)是将像素着色器中生成的各个片段的深度和颜色与帧缓冲结合在一起的地方。这个阶段也就是进行模板缓冲(Stencil-Buffer)和 Z 缓冲(Z-buffer)操作的地方。最常用于透明处理(Transparency)和合成操作(Compositing)的颜色混合(Color Blending)操作也是在这个阶段进行的。虽然合并阶段不可编程,但却是高度可配置的。在合并阶段可以设置颜色混合来执行大量不同的操作。最常见的是涉及颜色和 Alpha 值的乘法,加法,和减法的组合。其他操作也是可能的,比如最大值,最小值以及按位逻辑运算
2.2.8 The computer Shader
ompute Shader是shader的一种,他的存在是显卡渲染管线进化的结果:
- vertex & fragement shader对顶点和像素的计算是在GPU不同unit进行。
- 管线进化一个unit既可以运算vertex shader 也可以运算fragment shader。
- 进化到现在,每个unit不仅可以计算vertex & fragment,还可以用来计算渲染管线中其它的阶段。并且unit还有自己的memory,可以用来储存信息传递到其它shader stage。这个强大的unit叫做compute unit,而对应的shader,就叫做compute shader。
2.2.8.1 实现Compute Shader
Unity Compute Shader不像普通shader用material渲染物体,它必须由C# Script驱动:
- C# Script 创建渲染所需的信息包括模型、贴图、参数等然后调用Compute Shader
- Compute Shader计算出新的模型位置 对贴图进行处理 改变参数等
- Vertex Fragment Shader获得Compute Shader产生的信息进行渲染
1.C# Script
[SerializeField] Material _material;
public Material material {
get { return _material; }
}
[SerializeField, HideInInspector] ComputeShader _compute;
Mesh _mesh;
ComputeBuffer _drawArgsBuffer;
ComputeBuffer _positionBuffer;
ComputeBuffer _normalBuffer;
MaterialPropertyBlock _props;
Vector3 _noiseOffset;
2.开始声明 _material _compute _mesh 以及各项参数 为渲染准备素材
// Invoke the update compute kernel.
var kernel = _compute.FindKernel("Update");
_compute.SetFloat("Time", Time.time * _shuffleSpeed);
_compute.SetFloat("Extent", _triangleExtent);
_compute.SetFloat("NoiseAmplitude", _noiseAmplitude);
_compute.SetFloat("NoiseFrequency", _noiseFrequency);
_compute.SetVector("NoiseOffset", _noiseOffset);
_compute.SetBuffer(kernel, "PositionBuffer", _positionBuffer);
_compute.SetBuffer(kernel, "NormalBuffer", _normalBuffer);
_compute.Dispatch(kernel, ThreadGroupCount, 1, 1);
FindKernel函数按照名称找到Compute Shader中定义的一个运算unit,这里叫"Update"
SetFloat SetVector函数把脚本的输入和Compute Shader的输入关联在一起
SetBuffer把Compute Shader中定义的Buffer关联到外部,给其它阶段的shader提供数据
Dispatch函数是启动运算unit,这里就是启动名为"Update"的Compute Shader Unit
3.Compute Shader
#pragma kernel Update
RWStructuredBuffer<float4> PositionBuffer;
RWStructuredBuffer<float4> NormalBuffer;
CBUFFER_START(Params)
float Time;
float Extent;
float NoiseAmplitude;
float NoiseFrequency;
float3 NoiseOffset;
CBUFFER_END
#pragma kernel Update定义一个名为Update的运算kernel。一个kernel是数个Compute Shader unit运算模块的集合体,可以用来实现各种计算功能。
RWStructureBuffer定义了可读写的buffer,可以用来把信息传到其它渲染阶段。
CBUFFER定义了Compute Shader内部使用的固定变量,从脚本获得了输入值。
[numthreads(64, 1, 1)]
void Update(uint id : SV_DispatchThreadID)
{
int idx1 = id * 3;
int idx2 = id * 3 + 1;
int idx3 = id * 3 + 2;
.................................
PositionBuffer[idx1] = float4(v1, 0);
PositionBuffer[idx2] = float4(v2, 0);
PositionBuffer[idx3] = float4(v3, 0);
NormalBuffer[idx1] = float4(n, 0);
NormalBuffer[idx2] = float4(n, 0);
NormalBuffer[idx3] = float4(n, 0);
}
[numthreds(64,1,1)]定义了线程是如何分配的,如果是(8,8,1)代表每个线程组8个线程,共有8个线程组所以一共也是64个线程。
SV_DispatchThreadID当前所处的线程ID
因为每个线程处理一个triangle三个顶点,所以顶点ID idx1 idx2 idx3 为id*3 + 0 1 2
运算结束,返回PositionBuffer和NormlBuffer
Reference:
GitHub - keijiro/NoiseBall2: A small example of procedural modeling with compute shaders.
computer shader介绍:
Lele Feng:[Compute] More Compute Shaders
https://blog.csdn.net/csharpupdown/article/details/79385544
【风宇冲】Shader:二十八ComputeShaders_风宇冲_新浪博客
computer shader 工程:
https://github.com/lachlansleight/ComputeShaderTutorial
https://github.com/keijiro/NoiseBall2
compute shader 实例讲解:
https://blog.csdn.net/weixin_38884324/article/details/79285371
https://blog.csdn.net/asdasd4527567/article/details/82384207