找回密码
 立即注册
查看: 294|回复: 0

GPU大百科全书:第三章 像素处理那点事儿

[复制链接]
发表于 2022-8-9 20:22 | 显示全部楼层 |阅读模式
十年前,还在读研的时候,由于课题需要开始接触 GPU 编程。那时讲 GPU 底层机制、架构原理类的资料非常少,中关村在线顾杰老师的《GPU大百科全书》系列连载文章是当时为数不多的参考文献。喜欢探究底层原理的我,如获至宝的把这些文章读了又读,虽然当时看不懂,还是喜欢看。

首先是因为顾老师的文笔流畅,像讲故事一样将 GPU 的发展史娓娓道来,其中很多以当时的热门游戏为例,喜欢游戏的同学千万不要错过;其次是因为顾老师的这一系列文章涉猎广泛,真可谓是“大百科全书”,从图形渲染到通用计算,从架构演进到 A & N 纷争,从计算核心到通信总线,无所不谈,无所不包;最后,感觉其中很多话题有推测演绎成分,难免存在疏漏甚至谬误,这一点顾老师在系列前言中也已说明。如果你喜欢探究 GPU 的底层原理,相信这些文章可以以给你启迪和共鸣。

草蛇灰线,浮脉千里,一饮一啄,莫非前定。NVIDIA 当前在 AI 生态的统治地位早在二十年前就埋下了伏笔[1]。看完顾杰老师的这个系列文章,相信你也能找到其中的脉络和线索。这里把顾老师的原文转载如下,作为学习笔记,也希望能给需要的同学提供更好的阅读体验。
原文链接:GPU大百科全书:第三章 像素处理那点事儿

内容提要:在本篇文章中,你将了解到:
1. 为什么像素处理是 GPU 中发展最快的部分?
2. 像素处理与通用计算是什么关系?
3. 为什么早起使用 GPU 进行通用计算加速用的是渲染语言[2][3]?
4. 为什么要定义 grid / block?wavefront 和 warp 为何如此巧合?
5. 是什么推动了固定管线、可编程管线、统一ALU的架构演进,GPU 架构适合并行加速是巧合还是历史的必然?
五光十色的世界

前言:在之前的 GPU 大百科全书中,我们按照硬件流水线的顺序完成了几何部分以及光栅化部分的介绍,尽管上一期的大百科话题有些沉重,但整体上来讲,我们已经远离了光栅化这个凝固生命的过程。接下来,我们将跟随光栅化之后崭新诞生的图元生命,开始一段有关像素的异彩纷呈的新旅程。
与几何部分和光栅化部分若干年都没什么变化的情况不同,GPU 内部发展最快的部分就是像素处理部分。如果你是一个像素,你一定会为几年来自己周围环境的巨变感到兴奋,伴随着像素处理部分的演进,像素不仅被处理得越来越快,而且颜色也日趋准确和符合效果的需求。究竟这几年间像素处理部分发生了怎样的变化,像素的处理过程又有着哪些特点呢?接下来就让我们翻开GPU大百科全书新的章节来一探究竟吧。



颜色和明暗,是让我们这个世界变得美丽的一大原因,无论晨曦中朝露的璀璨,雨后彩虹的柔美,还是钢筋水泥森林的质感,无不来自于颜色及明暗所传达的强烈信息。想要在计算机世界中传递这些信息,我们需要一个最小的信息单位来最为载体,这个最小的信息单位,就是像素。



像素是构成图像的基本单元

我们面前的屏幕能够反映的最小的颜色点就是像素,大量像素按照正确的规律排列所形成的集合就成了图像。由此可见,如要想要让我们看到美丽真实,或者说正确的图像,像素的颜色必须也是正确和自然的。



图像的精确来自像素的正确

反应正确的颜色?这不是很容易么?每种颜色都有一个 RGB 值,只要让每一个像素都具有对应位置上正确的 RGB 值,颜色不就没问题了?对于固定的反映式输出,比如说照片来说确实如此,人们只需要通过感光元件采集自然颜色的 RGB 数值,然后将它们直接转化成像素就行了。但对于电脑,或者说 3D 虚拟世界来说,这显然是不可能的——我们所做的事情,是在没有对象的基础上凭空创造出一个数字的世界,这世界的每一点细节,每一滴颜色,全部都要由我们来决定,而我们所有的,只有脑海中无尽的想象,以及由现实中抽离出来的描述这些想象的数学关系。
用数学关系创造出来的世界,是什么样子的呢?
像素的精彩

对于大多数人来说,第一次明白“3D游戏”这个概念,大概是从 DOOM 开始的,而第一次明白什么是贴近真实的视觉效果,则是在 Pixel Shader 出现以后。下面这些,就是伴随我们成长的那些像素们。



3Dmark 2001 的 nature 场景



3Dmark 2003 的 nature 场景



3Dmark 06 的极度深寒场景

3Dmark 的测试场景精确的反映了这些年像素特效进化的过程。我们创造出来的像素世界,已经从最初的“美轮美奂”向着“栩栩如生”的方向坚定地前进了。



经由 Pixel Shader 处理的环境光照效果



经由 Pixel Shader 处理的 real time HDR IBL

这日趋真实,甚至已经可以以假乱真的图像显然不是 GPU 出现第一天就被呈现出来的。那 GPU 究竟是怎样具备的对象素的自由处理能力?这个过程又经历了怎样的曲折过程呢?
凡事总有第一次嘛

在可编程 shader 出现之前,人们对像素的操作实际上仅仅能够称之为染色。当时的我们可不像现在这么幸福,那时候对特效的处理只能通过固定的单元来直接实现,每一代 API 下所能够实现的特定的特效,都需要通过预先将其固化成固定指令的形式出现在硬件中,而对于像素的处理,也仅能局限于固化指令所能够允许的范围内,一旦像素进入管线,程序员就失去了对他的控制。因此,可编程 shader 尤其是 Pixel Shader 的出现,在当时是一件轰动的大事,Pixel Shader 的出现,标志着程序员在像素级层面上第一次具备了可以精确而且随心所欲的控制自己想要实现特效的能力。



DirectX 7 的不可编程像素流水线处理的画面

其实说起来,想要在像素层面上精确控制并不是一件非常复杂的事情。颜色的表现来自构成它的三原色的混合度以及透明度,而三原色以及透明度在计算机的世界里都是可以通过数字来精确度量和表达的。所以对于像素来说,只要能够随心所欲的处理构成它颜色以及透明度的 RGBA 这四组数字,就可以精确控制一个像素的颜色表现。这件事听上去似乎很简单,做起来就是另一回事了,尽管只要将固定管线替换成可以任意处理方程的可编程运算单元就能实现上述目的,但毕竟大家在固化单元里生活里很多年了,想迈出突破性的第一步是很艰难的。



DirectX 一直都会在程序员最需要的时候及时出现

这世界上其实并不存在什么无法克服的困难,关键在于人们有没有动力去克服它。能够控制像素这件事,不仅对程序员来说诱惑极大,对最终用户也有着非常重要的意义,更好的图形表现是行业发展下去的根本动力,而行业的蓬勃发展就意味着滚滚钞票的到来,所以大商人微软再次体现了它的无处不在,2001 年微软发布了全新的图形 API —— DirectX 8[4],正式将可编程 shader program 引入到了桌面图形界,并宣称透过 shader,人们可以实现电影工业中 CG 一般的真实特效。微软这一推动的作用是极其明显的,就算固化单元里的生活再怎么安逸宁静,大商人放话要求可编程单元来执行新的指令,不出来帮衬一下显然太不给面子了,于是,第一代 Pixel shader 单元也就应运而生了。



DirectX 8 的出现让很多波动的水面之类的特效得以实现

第一代完全符合 DirectX 8 要求的图形构架均采用可编程单元来替代固定指令渲染管线,算术单元的引进成了这些构架共同的特征,它们都通过支持 INT16 及 FX12 数据格式的 combine 单元来执行对像素 RGBA 数据的运算处理。使用可编程的具备直接执行能力的算术运算单元来代替固定指令处理像素,这让程序员得以实现许多过去根本无法想象的特殊视觉效果,通过直接使用数学关系来对应图形,真实的表面光照效果、界面半反射、散射以及折射等等视觉效果让人们彻底摆脱了单纯 alpha 贴图的乏味。可编程 shader program,尤其是 Pixel Shader 的出现,标志着人类正式进入了游戏应用向电影特效进军的时代。
run Forrest!run!

Pixel Shader 的出现是一件让所有人都欢欣鼓舞的事情,程序员们非常高兴,他们就像摘掉了有色眼镜的画家一样,终于拥有了可以正确控制所有像素颜色的可能,一般用户也非常高兴,他们如愿以偿的看到了光,看到了波光淋漓的水面,看到了各种各种各样以前不可能见到的新奇的特效。有了令人“叹为观止”的特效,消费者就会愿意为新游戏买单,于是整个业界也变一片欣欣向荣了。



nature 场景给很多人留下了极其深刻的印象

当你第一次看到 3Dmark 01 的 nature 场景时,你是否为能够生活在这个技术发达的美好世界而感到高兴呢?是的,能够在计算机的世界里看到如此美妙且连贯流畅的画面,而且还有人说今后一切特效都可以得到实现,自己掏的 cash 没有白费,这是一件多么美好的事情啊。包括笔者自己在内,大多数人在那个时代都有着这种幸福甚至是满足的感觉。



Pixel Shader 1.0 实现的水面半反射效果

当然,不是所有人看到新的 shader 效果之后都会那么的幸福和满足,比如刚刚还很高兴的程序员就开始犯愁了——奇怪,这图像怎么跟自己想象的不太一样啊?



shader modle 1.0 的精度依旧不足

第一代 Pixel Shader 确实为程序员打开了控制所有像素的大门,但它本身还存在诸多问题,其中最大的问题便来自精度。第一代 shader 所采用的 combine 仅能处理整型数据,对于浮点数据则无能为力,这极大地限制了数据处理的精度,进而影响到了数据背后的像素颜色的表现。数据的精度决定颜色的精度,当特效对像素的要求达到一定高度之后,对颜色的正确性也会变得更加敏感,整型数据的运算精度显然不能满足最终效果的要求,连数字都没算对,程序员当然看不到自己想象中的结果了。



由于精度不足,shader modle 1.0 的水面永远都是“波澜不惊”的

Pixel Shader 1.0 确实为人们带来了比过去更高的对像素控制的自由度,但它距离真正的自由还有极大的距离,与其说它可以实现一切特效,不如说它只能实现“错误的特效”。这种现状显然不可能令一直都处在高速发展状态下的图形界满足,当时图形界刚刚看到了自由的曙光,就好像甩掉助行器的阿甘,才品尝到奔跑带来的快感,显然不可能就此停下。所以大家一起通力合作,大商人制定路线计划和规则,众厂商奋力攻关,终于将 Pixel Shader 2.0 的硬件带到了人们的面前。



提高像素处理精度之后的效果

Pixel Shader 2.0 相对于前代最大的不同来自精度,在 Pixel Shader 2.0 中微软第一次引入了 FP24/32 浮点数据作为颜色处理的基本精度,而执行这些数据的硬件也从 combine 转变成了功能更加强大的 mini ALU。更加精确的浮点型数据让 RGBA 数值具备了充足的准确性,这对于最终效果的准确表达起到了决定性的作用。另外,Pixel Shader 2.0 还提供了更大的指令数,让程序有了比第一代 Pixel Shader 更好的执行效率。在 Pixel Shader 2.0 的帮助下,程序员就好像治好了近视一样,第一次达到了正确的控制像素以及控制正确的像素的程度,3D 图形终于完成了从“美妙”到“栩栩如生”的转变。
打开真实世界的大门

Pixel Shader 1.0 让图形界从固定的条条框框中跳了出来,但它存在精度不足的问题。Pixel Shader 2.0 弥补了精度方面的缺失,那它就完美了么?显然不。尽管 Pixel Shader 2.0 能够提供足够让画面达到人眼分辨上限的精度,但它却存在不比精度不足好到哪里去的问题——它太过笨拙了。



3Dmark 03 的 nature 场景已经足够美丽了

3D 图形不同于静态画面,连续动态画面的流畅度和画面的准确表现是同等重要的,如果硬件仅仅能输出准确的效果,却无法在输出速度上予以保证,用户获得的最终体现效果一样会大打折扣。随着 Pixel Shader 的逐步发展,其最大指令长度慢慢的从出现之初的 16 条发展到了 2.0b 的 512 条,这使得指令的执行效率成了摆在人们面前的现实问题。非常遗憾的是,Pixel Shader 发展到 2.0b 为止都没有引入真正有效的提升指令执行效率的手段,跳转、分支、流控制等今天看来很平常的东西在当时全都是不存在或仅仅存在于“支持”这种程度的,这让 Pixel Shader 的执行效率出现了极大的问题。



3Dmark 03 的 Pixel Shader 2.0 测试场景

与此同时,由于最大指令数本身的限制,程序员对每个像素所能够进行的改变实际上仍然会受到限制。如果想要添加更多更真实的效果,程序员往往需要让一个像素多次反复进入 Pixel Shader 单元中进行处理,这让本来就已经存在的效率问题变得更加雪上加霜。
好不容易能够表达正确的结果了,效率却跟不上,程序员们都觉得很不舒服。程序员不舒服,大商人的收入就会受到影响。于是注意到问题所在的微软推出了号称史上最完美的 DirectX 版本—— DirectX 9.0C。



Microsoft DirectX

在 DirectX 9.0C 中,MS 显然想将之前版本的 DirectX 中所遗留的问题一次性彻底搞定,Shader Modle 3.0 的最大指令数提升至 65535,增加了寄存器数量,引入了动态程序流控制,将分支和跳转能力彻底开放给了程序员,同时通过多目标渲染(MRT)和延迟渲染(Deferred Shading)等创造性的技术保证了光栅化过程中整个流水线的整体效率,可以说 DirectX 9.0C 几乎解决了 ALU 利用率和效率之外的一切问题,它成了历史上第一个真正能够“实现一切特效”的图形 API。



支持 Shader Modle 3.0 特性的 X-ray 引擎画面

得益于微软英明的指挥和领导,支持 Shader Modle 3.0 的图形构架拥有了远比之前构架更多的寄存器资源、接近无限长度的指令执行能力以及灵活的控制操作方式,因此也就具备了远比之前构架更高的执行效率。程序员们获得了比先前所有版本都多得多的自由度,他们终于可以在支持 Shader Modle 3.0 的硬件中近乎于不受约束的尽情堆砌指令,来实现越来越接近于现实的色彩和效果了。
从像素向计算的跨越

如果要评价这世界上最悲情的职业,我觉得程序员一定会以高票数当选的。程序员永远都得不到他们想要的理想环境,当他们遇到某些问题并最终等到了这些问题的解决时,往往会发现这些问题的解决其实只不过是引出了新的问题而已,像素的世界就是如此。Shader Modle3.0确实解决了先前版本对于指令长度的限制以及执行效率方面的缺陷问题,但急剧放大的指令长度以及其本身的出发点却将另一个问题非常明显的凸现了出来,那就是执行单元的总体效率问题。



彩虹六号:维加斯的执行效率已经受到了 Shader Modle 3.0 的制约

传统的 Shader Modle 中,Vertex Shader 和 Pixel Shader 是完全分立的两组 programs,他们拥有不同的寄存器要求,不同的指令格式以及不同的运算器要求。因此传统硬件只有使用专门的比例一定的 Vertex Shader 和 Pixel Shader 单元,将它们分开进行处理。这种举措本身从最开始就注定了很多不可调和的矛盾——固定单元比例的硬件凭什么就能达到程序员对 Vertex Shader 和 Pixel Shader 数量及分布的要求?Shader Modle 3.0 号称是破除一切限制的史上最自由的 Shader Modle,结果到头来还是要程序员严格按照硬件构架的大致比例和节奏来分配自己的 Vertex Shader programs 和 Pixel Shader programs,只要稍有出格,硬件马上会以直线下降的执行效率来回报你。



传统分立单元的效率很难得到保障

除此之外,这种固定比例还导致了非常严重的单元利用率低落的问题。假定一段 shader programs 中仅包含10%的 Vertex Shader 指令,剩下的 90% 都是 Pixel Shader 指令,那么当重载的 Pixel shader 单元全力动作的时候 Vertex shader 单元实际上是处在欠载状态的。这种情况反之亦然。一段实际的 shader program 是不可能完全做到 50:50 的指令平衡设计的,再加上指令的串行吞吐特性,程序员无论如何都不可能做到指令密度的平均化。因此我们不难发现,实际应用中根据传统的API设计出来的硬件经常会出现大面积的负载不平衡的现象。



被 MRT 以及 Shader 拖累的彩虹六号

正当程序员为自己一次又一次的看到自由的曙光,却又一次又一次的被打回到各种束缚中而黯然落泪时,一只手轻轻地递过来一张纸巾,程序员用纸巾擦拭着眼泪,却发现纸巾下面盖着一个金光灿灿的新 API——DirectX 10。程序员惊讶的半张着嘴巴,正准备说些什么的时候,微软微笑着用它特有的浑厚嗓音说:“不用问了,我叫雷锋!”



使用 Shader Modle 4.0 全新 GI 效果的《狂野西部》

DirectX 10 以及其所带来的 Shader Modle 4.0 可以说为图形界翻开了全新的篇章,它创造性的引入了 Unified Shader 的概念,将传统的 Vertex Shader 和 Pixel Shader 从软件和硬件层面上予以统一,shader programs 内部不再需要严格按照格式来区分 Vertex / Geometry / Pixel。软件变了,硬件也要跟着改,所以对于支持 Shader Modle 4.0 的硬件来说,其执行 shader programs 的单元也从过去的分立式固定功能变成了更加强大完整且完全统一的通用 ALU。因为 ALU 可以对全部 shader 进行无差别吞吐,整个硬件的执行效率第一次在理论上达到了100%。 



Shader Modle 3.0 与 Shader Modle 4.0 效果对比

Shader Modle 4.0 对像素来说有着极其重要的意义,它不仅通过统一 ALU 吞吐提升了硬件的执行效率,更让程序员们可以更加随心所欲的使用效率更高的指令。传统的 DirectX 9 硬件中的 shader 格式是非常固定的,Vertex Shader 指令天生就是 4D(X,Y,Z,A),而 Pixel Shader 指令因为硬件单元设计通常都是对应 RGBA 的 3D+1D 结构的缘故,一般情况下也会写成 4D。这导致了 DirectX 9 环境下的 Pixel Shader 指令无论属于何种应用,哪怕仅包含一条 Z-buffer 或者一条 texture load,也要在指令结构上找齐成 4D 格式。这种格式的刻板要求极大的限制了程序员对 shader 尤其是 Pixel Shader 的发挥。在 Shader Modle 4.0 环境下,统一且无限制的指令格式要求以及直接面向底层 ALU 的特点让程序员可以大胆的直接使用更加灵活的 1D、2D 指令以及各种算数函数,而不用担心任何来自硬件方面的限制,这让 Shader Modle 4.0 的效率和灵活度提升到了一个新的高度。



大量采用 1D 指令及算数函数的极品飞车 13

与此同时,由于对最底层运算单元的直接开放,人们忽然发现原来 GPU 里竟然蕴藏着如此丰富的运算资源。常规GPU 用来处理 shader 的大规模并行 ALU,本身可以轻松的拥有数倍甚至数十倍于 CPU 的吞吐能力,通过 Shader Modle 4.0 面向 ALU 的开放,现在程序员们可以通过编程手段将这些原本用于处理像素效果的运算单元拿出来进行数学计算。像素处理,终于开始了自己从图形向计算的跨越。
从计算到图形的回归

Shader Modle 4.0 是 Shader Modle 历史上的一大飞跃,它不仅让像素处理变得更加有效率,而且还让过去单纯处理像素的单元腾出手来处理图形之外的计算工作,使得整个 GPU 的用途和应用范围变得更加宽广,从任何角度来说这个版本都应该是史上最完美的了。如此完美的版本,难道程序员们终于可以摆脱自己悲催的命运了么?



苦中作乐的程序员

雷锋同志的表现是极其稳定的,它为程序员们带来的 DirectX 10 以及 Shader Modle 4.0 一如既往的从根本上解决了前一个版本所暴露出的一堆严重的问题,然后将另一堆同样严重的问题暴露给了程序员们,对程序员们来说,命运依旧是照旧轮回的,这次暴露给他们的东西包括了并行度和几何关联性问题。



并行 kernel 相对于串行的优势

在我们对像素进行处理时,每一个像素都会对应一个线程。随着指令的日趋灵活和复杂,以及每代 DirectX 所带来的效率提升等效化之后的更多视觉效果,分支、跳转以及常规算术函数等等的日益增多,都让线程队列的执行效率问题变得越发重要起来,这种情况在 Shader Modle 4.0 取消了对指令格式的机械限制之后变得更加明显了。大量的线程聚集成 CTA,进而成为一个 kernel,若干 kernel 形成队列,这些 kernel 由于待处理像素的不同以及内部线程的指令灵活度不同而变得不再同样“丰满”,如果 kernel 队列以顺序的方式一次一个的送入到 GPU 中,势必无法做到让整个 GPU 的所有 ALU 都满负荷工作。换句话说,Shader Modle 4.0 对指令的解放,就如同 Vertex Shader和 Pixel Shader 出现时一样,在为我们解决问题的同时非常传统的再次为我们带来了一个新的麻烦,因为很不幸的,Shader Modle 4.0 对 kernel 的吞吐,刚好就是依顺序方式一个一个来的……如何让线程能够尽可能的充盈所有的 ALU,减少ALU的等待周期并提高他们的重复负载率,是近两年 GPU 构架改进最重要的目的和任务。



Shader Modle 4.0 代码顽固的几何关联性

除了 kernel 之外,Shader Modle 4.0 还存在计算性能难以释放的问题。对,我没说错,你也没有看错,Shader Modle 4.0 虽然是第一个允许程序员直接使用 ALU 的运算能力做像素处理之外的事情的 Shader Modle 版本,但他也确实存在计算性能难以使用的问题。



想要使用 Shader Modle 4.0 的通用计算能力,你可能还要去学学 fortran

假如你是一个物理学家,想要用 Shader Modle 4.0 带来的运算能力来为你解决某个刚体碰撞问题,硬件会告诉你没问题,你只需先学习图形相关的 GLSL [2]或者 HLSL[3],了解图形处理过程以及整个图形流水线的特点,甚至还可能要跑去学点 fortran 什么的,然后将你所要运算的目标转化成图形线程,将之按照严格的几何关联性一一对应到图形过程,比如 Vertex、Texture 或者 Z-buffer 中去,最后再把弄好的你也不知道到底应该算是图形程序代码还是科学运算代码的东西打好包发给硬件。这样算是方便释放运算性能么?起码我不觉得是。



DirectX 11

有矛盾就要被解决,有困难就要找雷锋。于是在程序员们(这次还包括了大量非图形界的程序员)凄楚的目光注视下,微软再次拿出了解决上一个版本问题的全新版本—— DirectX 11 以及 Shader Modle 5.0 。在 Shader Modle 5.0 中,微软引入了两个重要的概念:并行 kernel 及 Compute Shader,前者通过引入 kernel 并行执行的形式来解决 ALU 利用率的问题,而后者则通过打破几何关联性的方式将 ALU 的运算能力真正释放了出来。



Compute Shader 实现的 1000 光源场景

并行 kernel 的作用和意义非常直白,就如同字面意思一样,并行 kernel 通过把不同的 kernel 以并行队列的方式发送给 GPU,达到提升其处理效率的目的。相对于并行 kernel 来说,Shader Modle 5.0 引入的 Compute Shader 要重要得多。取消了几何关联的 Compute Shader 是史上第一个完全开放的数学指令型 shader,Compute Shader 可以透过并行管理方便的实现数据共享,可以透过树结构和延迟操作快速执行任意过程,虽然丧失了几何关联所带来的各种自动功能让 Compute Shader 看上去与大多数图形过程绝缘了,但事实却恰恰相反。Compute Shader 的出现,不仅没有进一步的将通用计算和图形计算割裂开,反倒直接打破了传统的界限和束缚,将图形和通用计算彻底联系在了一起。有了 Compute Shader,显卡的通用计算能力不仅可以以最直接的数学形态被释放出来以帮助需要计算量的领域,而且还能很方便的直接被用于特效的处理,使其成为图形计算能力,ALU 在经历了 Shader Modle 4.0 短暂的运算能力分离之后,终于在 Shader Modle 5.0 完成了回归和升华。
像素的沉重灵魂

也许你会问,像素不就是颜色么,处理像素就是刷个油漆而已,屏幕上几个需要刷刷漆的点,何德何能让包括微软在内的整个图形界围着它们折腾了 10 年之久而且还要继续折腾下去呢?



实际像素操作可不是刷刷漆那么简单

如果一个像素被摆放在静止的空间内,周围的环境完全没有任何的变化,这个像素自然也就不会有任何变化。对于这样的像素我们甚至不用处理,直接以烘焙材质+纹理贴图的形式就可以完成表现了。但是,现实中的像素点肯定不会是这样的,如果你想要表达真实自然的颜色效果,这些像素就必然的会与光和其他像素发生关系,并在发生关系之后表现出正确的符合物理规律的颜色。而与光以及其他像素发生关系,就势必会导致复杂的处理过程,于是,也就有了这段长达 10 年而且可能会永远没完没了的纠葛。



3Dmark Vantage 的像素光照效果

以光照为例,在实际的应用中,对光照的操作有很多种方式,传统的方式大多是将光照信息直接对应到 Pixel Shader 指令的执行过程,比如 multi-pass render 及 multi-light single pass render 等。在 Pixel Shader 2.0 中,这些方式被用于处理不同场景的光源对物体的影响。如 multi-pass render pipeline 会为每个光源创建一个单独的过程用来执行,多用于室内环境处理,而 multi-light single pass render 能够在一个过程中同时处理 3 至 4 个光源,可被用于室外大范围表现的环境。



不同的光照会导致极大地真实度差异

不论采用哪种方式,在处理过程中都会对光线与像素的数学关系,也就是光线对像素颜色的影响以及透明度的影响,最简单的点光照改变像素颜色的公式可以写成 Color = Ambient + Shadow * Att * (N.L * DiffColor * DiffIntensity * LightColor + R.V^n * SpecColor * SpecIntensity * LightColor),这个最简单的公式中最少有四个分量,即 N.L、LightColor、R.V^n 以及 Attenuation 需要处理,对每一个分量的处理都会导致最少一个单独的指令,同时还要为这些处理过程搭配对应的 buffer 以便能够缓冲和临时存放中间结果,另外,由于颜色是三原色组成,因此 LightColor 还要被拆分成 LightColor.r、LightColor.g、LightColor.b 三个分量分开处理,最终再将它们合并在一起。



单点光源场景演示

折腾完这么一大堆的方程、变量和指令之后,你终于完成了一个像素一个点光源照射之下的颜色变化。如果是multi-pass render pipeline,这个像素也许就算是弄完了,要是赶上 multi-light single pass render,后面还有长长的其他光源以及多光源符合效应等一大堆过程在等着呢。



全局光照场景演示

一个像素尚且如此,我们的屏幕中存在着 2304000 个像素(1920X1200),即便假设其中只有 1/4 需要处理,那也有 576000 个像素,即便所有这些像素都只被一个点光源照耀着,而且全部没有其他的交互作用关系,我们也要把上面那些步骤整体重复 576000 次才算完,这其中的运算量到底有多大,诸位可以想象一下。而如果对效果的要求很高,比如说将漫散反射定义成新光源,那么每个像素所要处理的运算量都将因为光源的激增而急剧加大,即便加入光照探针之类的手段,这巨大的运算量依旧会给现有的常规硬件带来很大的压力。而实际的游戏应用中,我们面对的效果显然不止漫散反射光源这么点而已,大量的像素间的交互作用一样要被处理,这种种处理需求加在一起,就构成了像素沉重的灵魂。



大量使用复杂光照效果的 Crysis

现在,你应该明白为什么微软带着大家忙活了十来年,也没搞定刷漆这么个简单的活了吧。
像素到效果的桥梁

尽管恩怨纠葛持续了很久,硬件的结构也越来越复杂,但像素本身其实还是非常单纯的。我们说过,对像素的处理,归根结底是对构成颜色的三原色的处理,而对三原色的 RGB 值以及透明度 Alpha 的量化处理,实际上是非常单纯的数学运算。这个单纯的计算过程,为什么会衍生出了如此纷繁复扰的处理过程的呢?像素到效果之间,究竟跨越了怎样的阶段呢?



Grid 到 Thread 所对应的硬件

在前一页中我们已经知道,如果我想将材质中某个原本黄色的像素点揉到光线效果中并令其因此改变颜色,需要将像素原本颜色的数值以及它与光线和其他通过数学的手段处理成一个或一组方程。这就是像素向效果迈进的第一步,从像素变成方程。不同的像素改变对应了不同的方程或者方程组,要处理这些方程/方程组,就必须将它们转化成ALU能够接受的形式,因此方程/方程组就变成了指令。



像素线程的分块

接下来,硬件流水线如果要执行这些指令,就必须让其拥有符合流水线特征的身份,于是以 1 个像素为单位,这些指令被转换成了 Thread(线程)。要实现某种特效,显然不太可能只改变一个像素,于是一堆像素的改变就变成了若干 Thread 的集团,为了方便硬件进行吞吐,若干 Thread 会合并成为 CTA(线程块),它对应硬件能够执行的最小粒度,也就是 NVIDIA 的 warp 或者 AMD 的 wavefront。若干 CTA 又可以组合成一个 Block,这是方便程序员进行发放管理而设定的最小 Thread 单位。一个最小 GPU 执行单元比如 SM 可以面对若干个 Block,当一个Block 里的所有 CTA 都被执行完之后,SM 就会寻找其他未被执行的 Block,被划分在一起可以由同一个 SM 完成吞吐的 Block,被称作 Grid(关于 Grid 定义我保留疑问)。



对全屏幕像素的网格化(Grid)

最后,很多特效都是基于全屏基础的,换句话说,屏幕内会出现大量需要更改的像素,这些像素更改所对应的全部过程就是一个 kernel,这个 kernel,就是由所有 Grid 组合成的集合。



从像素到画面的转变

你觉得有些头晕?没关系,我换个可以帮助理解的方式——因为我们要理解的是像素到画面之间的递进关系,所以我们只看像素即可。要实现特效,所要改变的最基本单位首先是像素,一个改变的最终结果对应一个像素任务(Thread);出于提高 GPU 的像素吞吐效率的考量,若干个像素会被组合成一个整体集团被 ALU 团簇执行(CTA);既然方便了硬件,程序员也要得到便利,所以我们又设定了一个程序员方便管理的最小单元,里面包含了若干被组合在一起的像素集团(Block);一个 ALU 团簇一次能够面对若干个这样的像素集团,他们也可以被称作一个更高级的集合体(Grid);最后,当屏幕内所有的这种最高级待改变像素集合体被执行完毕之后(kernel),特效就呈现在我们眼前了。
表面的把戏

我们知道,在 GPU 的图形流水线处理过程中,像素处理是排在光栅化过程后面的,当几何模型完成 3D 到 2D 的坐标变换之后,留在流水线中的就只剩下原几何模型中可以被摄像机镜头看到的那部分表面的投影了。因此,对这部分范围内像素效果的处理,也可以被理解成是对表面颜色的操作。



光栅化之后的三角形

我们在前面对 shader modle 的回顾中提到过,在 DirectX 10 以前,这部分运算操作由专门的含有 combine 或者 mini ALU 的 Pixel Shader 单元来完成,而 DirectX 10 之后,则可以简单的理解成由 ALU 直接完成。接下来,就让我们轻松一下,来看看对表面的处理流程究竟是怎样的吧。



DirectX 10 图形流水线

当经过光栅化的模型投影,或者说图元出现在流水线之后,材质单元会根据程序的要求对图元进行区域划分定位,然后从材质库中寻找对应表面区域的材质,将其拾取出来贴到已经 2D 化的模型平面对应的区域内。与此同时,ALU 则要根据程序的要求,对不同的像素区域中的纹理进行对应的操作,比如光照探针侦测、光线关系判断等等。需要注意的是这里的所谓光线关系并非仅仅是明暗之类光照度以及光照角度那么简单,这其中还包括半漫反散射以及折射之类光线传递关系的效果。在完成上述关系的判断之后,ALU 会按照结果执行程序包含的描述这些关系的对应方程,并最终经过对方程的运算得到某个像素正确的颜色数值。最后,处理完的结果将被传送至 ROP 单元,它会将处理好的像素与已经存在的基本材质进行混合,然后就可以输出我们能够看到的最终效果了。



标准的光栅化图形处理流程

不难发现,对物体表面效果的处理并非 shader / ALU 一己之力完成,它实际上是与纹理单元共同完成的。尽管 shader / ALU 完成了其中最重要最复杂,同时也是最为考验运算能力的环节,最终的表现结果都是由 ALU 决定的,但整个过程无法离开纹理单元以及材质的参与,这是为什么呢?
像素离不开纹理

既然像素处理过程就是处理像素,那我们为什么还要现在物体表面蒙上一层预先烘焙好的材质作为基础呢?反正这些材质上大部分的颜色都不是正确的,到头来还是需要 ALU 对其进行运算并完成修改,那为何不直接让像素处理单元直接在正确的位置上生成正确的像素呢?这样既可以避免改错这么一个看上去似乎没有必要的步骤,又可以用原本进行材质操作的单元的晶体管来进一步强化 ALU 部分,让其拥有更强大的功能,何乐而不为呢。



shader 与 Texture 是一对“好碰友”

答案很简单——因为现在的 ALU 根本没有那个本事面对直接生成像素所带来的运算量。
我们在本文中反复强调过,像素的处理过程从本质上来说并不复杂,其巨大的执行难度并不来自步骤的繁琐,而是来源于对大量像素进行数学关系运算所导致的运算量,这 10 年来针对 shader 反复的折腾其实也只是因为人们对执行单元能够更加高效的处理数学关系的渴求。在以前的文章中我们曾经面对过类似的问题,当方程的数量达到一定级别之后,对于运算单元的压迫将让任何本来看上去很和谐优雅的方程式变得丑陋无比。而随着程序员和用户对效果要求的不断提升,对于像素处理的方程总量在未来将呈现出明显的只增不减的态势。更加复杂真实的光照模型,更加多变且逼近现实的光线传递效果,甚至包括更多像素透明度遮蔽所带来的混合,这些都让目前的 ALU 单元承受着巨大的压力。在这种情况下,要让 ALU 去独立生成全新的像素,为 ALU 添加一个近乎于 100% 增幅、甚至比这个还要大得多的像素处理压力,显然是不现实的。



材质操作可以大幅减轻像素处理的负担

为了减轻这种压力,人们历经 10 年,不断地压榨着半导体工艺的极限,以期能够在 GPU 内部塞下更多的运算单元;不断地改进着运算单元集合的逻辑关系,以期能够让它们尽可能高效率甚至全功率的运作起来;不断地开发着如 Compute Shader 之类能够尽可能多的以灵活的运作手段和更加贴近纯数学的应用方式来解决问题的方法,以此来进一步提升 ALU 在处理过程中的效率,并进一步压榨 ALU 的价值。对于材质的操作,也是这些努力中的一部分。预先烘焙好的材质可以带来大量已经具备基本效果关系的像素,这些操作上相对廉价的像素中会有相当一部分不需要被处理,直接使用材质可以大大减轻 ALU 单元的负担,让已经不堪重负的 ALU 得到喘息。



现代游戏均大量采用像素结合材质的处理方式

所以,在我们可见的未来中,对于 shader 的处理依旧只能跟材质操作紧密的联系起来,Direct Pixel 这种东西,相对来说还是一个遥远的梦想。
下一章是……番外篇?

根据惯例,每一章 GPU 大百科全书的结尾,我们都会对本章的内容进行小结,总览一下该章所介绍的单元,总结和评价它在图形渲染流水线中的作用和地位,并且承前启后的引出流水线上的下一个单元。但是今天,我们要打破这个惯例。因为关于 Pixel Shader 以及相关单元的故事,其实还远未结束呢。



像素处理单元是一个有故事的地方

对像素的处理,本质上就是对色彩相关的方程的处理,这种处理的背后又附庸着大规模的数学运算和大量的指令执行,所以像素对于硬件的逻辑结构设计,存在着极高的要求。不同的逻辑结构对于庞大数学运算和指令的吞吐以及处理能力,显然是存在差距的。每一家硬件厂商对于数学以及效率的理解都不相同,设计出来的硬件也存在着极大的差异。



ALU 团簇结构的设计分歧

另外,我们的活雷锋大商人微软同学非常顽皮,虽然他带领着大家不断的改进着像素的世界,但爱耍小性子的他有时候会跟这位小同学走得近一些,有时候又会和那位小朋友玩的好一点,这就导致了微软在新 API 规则制定时或多或少的倾向性,以及大家对每一代 API 意义及精髓理解方面的差异。



ATI vs. NVIDIA

好了,有差异,就会有分歧,有分歧,就会有争执,当争执最终达到了商业竞争的你死我活以后,战争也就爆发了。
自 shader 出现至今,围绕着 Pixel Shader 以及相关单元设计方面的激烈竞争,可以说是人类 IC 史上最为灿烂的一场大碰撞,这 10 年间 ATI[5] / AMD 与 NVIDIA 的此消彼长,不仅带来了甚至已经事实上打破摩尔定律的技术进步,更我们上演了一幕又一幕惊心动魄又异彩纷呈的战争大戏。宣传战、精度战、谍报战、工艺战、芯片策略战、产能战……相信经历过的人都会觉得,这 10 年间发生在 Pixel Shader 以及相关单元身上的种种奇遇,绝对可以写成一部相当跌宕起伏的小说或者剧本了。



GPU发展还能有谍报战搀和?

在下一章的GPU大百科全书中,我们将继续关于像素的未讲完的故事。这个特别准备的番外篇最终会让你明白,究竟是怎样巨大的差异,能够让一个处理像素的单元的设计工作迸发出如此耀眼的火花,这火花又是多么的夺目璀璨。敬请期待吧。
GPU 大百科全书索引


  • 第一章 美女、方程与几何
  • 第二章 凝固生命的光栅化
  • 第三章 像素处理那点事儿
参考


  • ^英伟达高管交流纪要https://xueqiu.com/6846564531/223664454
  • ^abGLSLhttps://en.wikipedia.org/wiki/OpenGL_Shading_Language
  • ^abHLSLhttps://en.wikipedia.org/wiki/High-Level_Shader_Language
  • ^DirectXhttps://en.wikipedia.org/wiki/DirectX
  • ^ATIhttps://en.wikipedia.org/wiki/ATI_Technologies

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 10:30 , Processed in 0.094882 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表