GPU渲染管线和架构整理(二)

前言

承接前文GPU渲染管线和架构整理(一),本文继续对腾讯技术工程号发布的文章《GPU 渲染管线和硬件架构浅谈》的后续部分内容做了梳理,同时也按照立即的理解好恶进行了一定程度补充,其中可能存在谬误的地方还请指正。总的来说这一段内容相当重要,原文花了重墨在Early-Z光栅化Tiling等细节上,与曾经所知结合一二,相应之下可谓豁然开朗。好了废话不多说,我们开始吧!

其他重要概念

1)Pixel Quad
  • 光栅化阶段离散化输出的单位不是单个“像素”而是“Pixel Quad”(2X2像素大小)
    • 参考上图,2X2 组成一个Quad,三角形只要覆盖其中一个像素,那么整个Quad都要执行像素着色器PS
    • 那些未覆盖三角形,但是被连带进入ps的像素叫“辅助像素”
      • 比较小的三角形渲染时,“辅助像素”占比一般会比较高 -> 这不好
      • “辅助像素”带来的问题在于
        1. 占用SM内计算资源
        2. 占用 cache line 带宽(沟通GPU和VRAM的通道)<-- 重点
      • 结论:尽量避免绘制大量“小图元”
        • 粒子系统闻后惊呼不妙(!!)
  • Early-Z 判定的最小单位也是 Pixel Quad
  • 设计“Pixel Quad”的一项作用
2)Early Depth & Early Stencil
  • 执行Early-Z/Early-Stencil/Early-MultiSampling的硬件与执行Normal-Z等操作的硬件是同一套
    • 所谓Normal-Z就是正常渲染管线中PS后执行的深度测试(Z-Test
  • Early-Z的执行时机是在完成图元的光栅化之后(After Rasterization)
    • 如果有更加“粗粒度”的预剔除技术(诸如Hi-Z),那么Early-Z也在“粗粒度”剔除之后执行
    • Early-Z必然在PS/FS之前
    • 朴素的流程 (简直和正常Z-Test一样,只是时机不同):
      1. Read/Load Depth-Buffer
      2. Do Test!
      3. Conditional-Write (back to Depth-Buffer)
    • Depth-TestDepth-Write皆原子操作
      • 本质是调用了 interlock opeation
      • 在 Read Depth -> Test -> Conditional-Write 期间 interlock 将一直生效
        • 注意,这个原子锁不会“横跨”整个PS阶段(即便硬件厂商可以这样做)
        • 因为这会导致在当前Pixel Shader完成输出前,系统不得不阻止所有其他渲染核心对当前像素的Shading <- 这不好
      • 锁的存在 -> 直接决定了渲染队列中的执行“顺序” : ARB_fragment_shader_interlock
        1. 这种“顺序”是Depth-Test所要求的 -> 不然会出现错误
        2. 这种“顺序”可用于解决多Shader处理同一Pixel时顺序不一致引起的问题
    • Depth TestStencil Test 也是同属一个硬件单元驱动(Raster Operation中的硬件单元)
      • 能做 Early-Z,就能做 Early-Stencil -> 所谓的 Early-ZS
      • 此外也涉及 Multi-Sampling
    • 对比分析Early-Z
      • 传统(没有Early-Z):
        • 深度测试发生在像素着色(PS)之后,Blend之前,当发现遮挡则Discard
        • Discard得越多,Overdraw越多 -> Sooooo bad!
      • 引入Early-Z技术之后:
        • Early-Z 测试发生在光栅化后,像素着色(PS)之前
        • 如果测试失败则直接不进入PS
        • 本质是避免Overdraw -> Sweeeeet!
        • 但是为了真正做到避免Overdraw,还需要从前(距离Camera近处)往后(距离Camera远处)排序渲染
          • 每一帧都需要对待渲染图元进行排序(sorting
          • 排序可能但不限于受到“运动的相机”+“可能运动的Geometry”的影响
          • IMRTBR管线架构:sorting由应用层执行,管线只负责按照提交顺序(submission order)执行
          • TBDR管线架构:sorting “可以”由硬件支持完成
            • 以PowerVR为例,完成sorting的主要功臣是: “Hidden Surface Removal” + “Deffered Rendering”两项技术
              1. 先阅览过所有不透明图元对应位置的像素后信息 -> 对应HSR
              2. 后放行“可见”的PixelQuad参数(独一份,利于片上缓存)-> 对应Deffered Rendering
            • Arm的Mali采用的“FPK”技术则依赖应用层排序,没有硬件级别sorting的支持
    • Early-Z 要点总结
      1. 尽可能提前剔除无效片元,提高整体渲染性能
      2. 剔除的最小单位是“Pixel Quad
      3. Early-Z 执行期间能不能"不要"写入深度?
        • 当然要,不然拿什么和当前Pixel的深度做比较
        • 能否只写入另开辟的临时深度缓冲?-> 会增加带宽和内存消耗,增加复杂度,没必要
        • 注意 Early-Z 本质上使用了 Normal-Z 完全一样的物理硬件
          -> 因此也很难在不改动电路的前提下,执行区别显著的逻辑操作
      4. Early-Z关联的优化不仅仅只对 Depth-Test部分起作用
        -> 还涉及Stencil,Multi-Sampling等相关硬件支持的其他测试
      5. Early-ZNormal-Z 一样,深度测试通过则触发深度写入,失败则Discard
        -> 同理,Stencil-Test 触发Discard后同样不会激活后续的 Depth-Test
      6. 一般只在渲染不透明物,且shader中没有原子操作,没有深度修改,没有Discard(glsl)/Clip(hlsl)指令时
        -> 才能顺利激活 Early-Z
      7. 在激活Early-Z的前提下,渲染框架可以选择不再开启 Normal-Z(避免重复劳动)
      8. Alpha-Test 会失活 Early-Z的一点释疑
        • 开启Alpha-Test就需要等到PS结束后才能获取到正确的Alpha值,并以此决定是否丢弃片元
          • 通过Early-Z测试,并触发了深度写入的片元,完全可能在 PS 阶段被无情Discard掉
            • -> 而这个片元形成的像素,在传统Normal-Z管线下,根本不应该执行后续的Stencil-Test深度写入...
            • -> 错误的根源在于 Early-Z 过滤掉了正确的像素片元,唯一通过的像素片元则会在后续PS阶段被剔除,从而产生错误!
      9. 开启 Alpha-BlendAlpha-Test 会使得管线严格按照应用层提交的队列顺序执行渲染
        • 在高度并行化设计的GPU内部,一般通过 interlock 确保这种顺序
        • 一个 PS 必须完成深度写入之后,相同位置的后续片元才能继续执行(读取正确的深度缓存,判断并尝试写入自己的深度)
      10. 如果Shader中存在PS阶段修改了深度的操作,也会失活Early-Z
        • 原因显而易见 -> 正确的深度只有在PS结束后才能获得
        • 任何之前进行的基于深度的测试都可能导致错误的表现
      11. Alpha To Coverage 也会失活 Early-Z
      12. 渲染API可以通过设置
3)Hierarchical-Z 和 LRZ
  • Hierarchical-Z也称Z-Culling,是Nvidia硬件支持的粗粒度Early-Z裁剪方案
    • 精度一般比Early-Z低 -> 精确到 8x8 的像素块
    • 但是贵在速度快,效率高
    • 作为Early-Z之前的预剔除
  • Low-Resolution-Z也称LRZ,是Adreno的类似解决方案
  • 基本运行逻辑(以LRZ为例
    • Tile-based-defferred 渲染流水线中的 Binning Pass 阶段产生一张低分辨率的全屏可见性缓冲
      • Full-Screen Visibility Table
      • Table的分辨率对于 LRZ 来说可以低至2x2(一个PixelQuad)
      • Table中含有
        • 来自于场景中图元的顶点信息(Vertex Position Info)
        • 其他未知辅助信息(待考)
      • 深度值正确性
        • 利用建立Tiler过程中的图元转换流程,获得所有覆盖在当前LRZ-Tile上的图元顶点信息
        • 在写入Table过程中“可能”附带有可见性排序
        • 本质是逐Tile跑了一遍 Vertex Shader 中的 Position 部分,得到低分辨率深度图
    • Tile-based-defferred渲染流水线的Rendering Pass阶段
      • 读取System Memory中存放的Visibility Table
      • 判断/确定图元的遮挡关系(可见性)
      • 按顺序光栅化图元(Rasterization of Primitives)
      • 走正常的Fragment Shading -> Z-Test -> Blending等流程
  • 导致LRZ失效的应用场景(主要应为片元着色器存在颜色输出之外的“副作用 Collateral Effects”),可以看出和Early-Z的失效条件基本一致
    1. writing to SSBO -> 片元着色器读写了SSBO,从而可能影响到其他渲染流程
    2. atomics op -> 片元着色器激活了原子操作 -> 通常是因为Alpha-Test所致(原因前文已讲)
    3. enable blending -> 激活了颜色混合 -> 天然要求不丢弃该片元
    4. modify fragment's depth -> 修改了深度缓冲 -> 同样会影响其他着色器的渲染输出
4)Tile-based Rasteration
  • 光栅化是对图元中三角形等基础几何构型(线段,三角形等)在投影平面上的离散化过程
    • 光栅化是逐图元逐三角形进行的
    • 光栅化输出是一系列PixelQuad,及其关联信息
  • 基于Tile的光栅化本质如下 (来源资料):
    1. 一种理解方式为:
      • 先划分Tile(可视为初步光栅化)
      • 然后在Tile基础上再次光栅化到正确的分辨率
    2. 另一种理解方式如下:
      • GPU并非是一次性将三角形按照渲染分辨率拆解成为Fragment,而是以更低的分辨率,比如1/8目标分辨率,进行光栅化。
      • 比如,画面最终是1920x1080,则GPU首先是以240x145这个分辨率进行光栅化,然后再对每个光栅化结果(8x8像素)进行进一步光栅化。
  • 这种分步光栅化的好处
    • 如果低分辨的一个单位整体上在Early-Z测试或者Early-Stencil测试当中被拒绝,那么就没有必要再对其进行更加精细的光栅化。
    • 可以认为和上文提及的LRZ/Hi-Z等是“一体两面”的技术
  • 关于Rasteration的几点补充
    • VS输出的三角形,在光栅化模块进行光栅化之后,形成PS/FS工作量
    • 光栅化之前,会进行:
      • 三角形级别的背面/正面剔除
      • 视锥剔除/裁剪
      • 以及零面积/小三角形剔除
    • Early-Stencil 工作在第一次光栅化完成后
      • 由于不可能完全与TBDR中的Tile相互对齐,大概率没有办法拒绝任何一个8X8的像素块(粗粒度剔除)
    • Normal-Stencil 工作在 Fragment Shader 完成后
      • 过滤的最小单位就是一个“像素”
      • 但是此刻工作负担并没有本质减少
  • 一个问题:如何处理大量小三角形绘制
    • GPU 非常不擅长这种工作场景
      • 优化手段有限
      • 光栅化输出的是 PixelQuad -> 极大增加了“辅助像素”占比
    • UE5 -> Nanite (UE5 渲染技术简介
      • 使用 Compute Shader 自己实现软光栅
      • 替代硬件光栅化处理这些像素级“小三角形”
      • 大概率取消了 PixelQuad
      • 相对传统光栅化大幅提升了处理性能 <- 特事特办!
5)Active Warp 和 Register Spilling
  • GPU一组SM所拥有的寄存器很多,但不是无尽的,所以系统设计时必须考虑到当Shader请求占用过多寄存器时可能遇到的情况和解决方案:这就是所谓的 “Active Warp”和“Register Spilling”的由来
  • 寄存器文件用多少,在Shader编译完成后就确定了
    • 每个变量、临时变量、部分符合条件的Uniform变量都会占用寄存器文件
  • Active Warp
    • 我们知道GPU系统为了最大化利用一组SM的全部32个Cuda Core,会把硬件资源分配给若干个逻辑上的线程束“Warp
      • 一组正在运行的Warp遇到长耗时内存指令时,为了“遮蔽延迟”,会激活另一个等待中的Warp
      • Warp之间切换的消耗非常小
        • -> 因为有大寄存器设计
        • -> 无需存档+重置寄存器
      • 当运行在一组Warp上的Shader要求了太多的寄存器时,必然会挤占其他Warp的可用资源,导致同一组SM资源只能分配更少的Warp
      • 这种视Warp所需资源不同,而动态分配SM上工作的Warp总数的方法就叫做“Active Warp
      • 极端情况下一个SM的所有Register不够一个Warp中的Shader用,那么这个SM就只能包含这一个Warp
        • 延迟隐藏”--> 会减弱甚至失效
        • Stall”-> GPU的硬件资源不得不空转等待
  • Register Spilling
    • 当寄存器不够用,或者Shader要求的寄存器数超过了某个阈值(比如64, 128等),就会触发Spilt
    • GPU会将 Register File 存储到 Local Memory
      • Local Memory 就是 System Memory 上的一块工作区域,访问速度属于GPU内最慢的一档
    • Register Spilling一旦发生,Shader执行性能会大打折扣
  • 检测方式
    • Mali offline compiler [参考]
6)Mipmap
  • 一个Mipmap纹理就像一座金字塔:
    • 基座是原始分辨率 (Lod 0
    • 每往上一层长宽各自减半(2的幂律)(Lod + 1
  • GPU在运行时通过ddxddy偏导求取当前像素覆盖的面积占图元对应uv的比值,从而估算出合适的Mipmap Level
  • Mipmap有利于节省带宽
    • 这并不是说我们传递给 GPU 的纹理数据变小的(相反是增加了)
    • 具体原因是:
      • Mipmap可使得渲染时相邻的像素更加可能处于同一个 Cache Line 里面
      • 从而变相提高 Texture Cache 的命中率
      • 也就大量减少对主存的交互,减少带宽
    • GPU 性能分析工具优化游戏性能的时候 Texture L1/L2 Cache Missing 是一个非常重要的指标,通常要控制在一个很低的数值才是合理的
  • Mipmap只比原始纹理的大小多了1/3的空间
    • 相对的,负责处理各向异性的Mipmap则要多出整整3
    • 具体参考这里
  • Unity Texture Streaming
    • 机制:通过引擎动态控制纹理的最高 Mipmap Level,从而控制纹理占用的总内存消耗
    • Texture Streaming可以确保纹理占用的内存总量是固定的
      • 因为可以把不重要的纹理换出成高Level的Mipmap纹理
      • 当需要低Mipmap纹理时触发重新加载过程
      • 全部引擎内部实现,上层开发者无感
    • 表现:很多3D游戏图片会有一个从模糊到清晰的过程
  • 移动端CPU和GPU使用同一块物理内存的不同分段
    • 通常情况下,如果纹理从CPU端upload到GPU端后,CPU端的资源会被释放
      • 此后我们再将显存中的纹理释放 -> 相当于在内存中释放了这个纹理
    • 如果一块纹理是需要CPU端读写数据的,或者编辑器下导入纹理后勾选了Read/Write Enabled
      • 即便upload纹理资源到GPU端,在CPU端仍然不会释放这些Texture
      • 实质上纹理资源存在两份(CPU一份,GPU一份)
7)纹理采样和纹理过滤
  • 纹理过滤的几种模式
    1. Neaest Point Sampling (临近点采样)
    2. Bilinear Interpolation(双线性插值)
    3. Trilinear Interpolation(三线性插值)
    4. Anisotropic Filter(各向异性过滤)
  • 现代GPU都支持在 1x cycle 内完成一次 Bilinear Sampling
    • 从性能上说:Point Sampling = Bilinear Filtering
  • 较新的高端GPU(如Mali-G78)
    • 可以在0.25x cycle内完成一次Binlinear Sampling
    • 也就是可以在 1x cycle内完成一个 Quad的采样
  • 对于三线性插值,由于需要使用双线性采样 2x Mipmap Texture,然后再做插值
    • 消耗是 Bilinear2x
  • N倍各向异性 -> 就是N倍的 Bilinear Filter 开销
8)硬件视角梳理
  • 可编程元件和固定管线元件
    • 顶点和像素处理是可编程,在 Shader Core 中执行着色器指令
    • 光栅化是不可编程的,由光栅化引擎负责
    • Early-ZNormal-ZBlend,是固定管线,由 ROP 单元负责
    • 固定管线的单元负责特定工作,硬件制作更加简单,性能更好,功耗更低
  • Early-Z
    • 每个 DrawCall 有它对应的 RenderState,以此来决定是否是 AlphaBlend、是否要深度写入、是否是AlphaTest
      • 但是对于硬件而言,每个图元并不知道自己是不是 AlphaBlend
      • 当前 RenderState 是 AlphaBlend 的话,那么图元就按照 AlphaBlend 绘制
      • 当前的 ZWrite 是 Off 的,那么 Normal-Z 就不写深度
    • 执行Early-Z 的是硬件单元(ROP
      • 所以不应该用代码的思维去理解 Early-Z 的执行过程
      • 更恰当的比喻应该是流水线上的阀门,它可以控制片元是否通过
      • 有一些我们用软件实现起来显而易见的算法,在硬件上却是非常昂贵,难以实现的方案
  • GPU核心的乱序执行和保序
    • GPU 的计算核心是乱序执行的,不同 Warp 执行耗时不一致。
      • 受分支、Cache Missing 等因素影响 -> GPU 会尽可能填充任务到计算核心
    • 但是同一个像素的写入顺序是可以得到保证的
      • 先执行的 DrawCall 的像素一定是先写入到 FrameBuffer 中的
      • 不同像素的写入顺序通常也是有序的
    • GPU 在每个阶段的输出结果其实都是有序的
      • 不同阶段之间,通过FIFO 队列,保证顺序
      • 随着技术的发展,可能使用的技术不限于FIFO,但是最终目的都是保序
    • 这个机制是有现实意义的
      • 对于半透明物体,如果 ROP 是乱序的,那么得到的是错误的结果
      • 而对于不透明物体,虽然有 Depth Test 的机制,乱序也可以保证结果正确
      • 但是保序对性能有好处,且可以缓解 Z-Fighting

下文承接:GPU渲染管线和架构整理(三)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 本文是对腾讯技术工程号官方文章《GPU 渲染管线和硬件架构浅谈[https://cloud.tencent....
    bbccyy阅读 2,381评论 0 2
  • 图形渲染管线是图形渲染引擎物件绘制的流程逻辑,平时在工作过程中经常会碰到各种跟管线相关的专业术语:前向渲染(For...
    离原春草阅读 5,485评论 2 12
  • 光栅化渲染管线是学习图形学的基础,学习渲染管线流程时,如果对其中的各个关键步骤理解不够深入,可能会看得一头雾水。这...
    太刀阅读 1,862评论 0 2
  • 1 简介 1.1 像素 屏幕是由许多小的方格组成,这就是我们常说的像素,由于方格很小很小,所以看上去像一个“点”。...
    backward阅读 1,283评论 0 5
  • Application Stage: 输入primitive(点,线段,几何体)即vertex data。 Ver...
    XY9264阅读 539评论 0 0