• 移动平台 GPU 架构扫盲


    一、硬件平台 EarlyZ

    1.1 TBR 及 TBDR 架构资料参考

    TBR 和 TBDR 架构网上的文章实在太多了,并且不同的硬件,内部具体的实现原理都不太一样(例如 Early-DT 的实现技术对于 PowerVR 是 Hidden Surface Removal (HSR),对于 Adreno 是  Low Resolution Z (LRZ)……)更何况细节,虽说这些都不算机密,但是也不是说你想拿到就拿到,除非是像这种相当专业的专利文,因此网上极大多数文章也都只能说是对各种渠道信息的总结和搬运,不能保证100%正确性,甚至还有些内容只是单纯推测,尽管如此,它们依旧是有不错的参考价值的

    这里不会再对一些比较常识性的东西进行简述和总结,只看单一篇文章也必有疏漏,因此直接贴链接供完全不了解这块的人做个参考好了,当然这种分析硬件架构的文章是没法原创的,说到底都是拾人牙慧,好听点的话叫翻译和总结:

    1.2 但是还是要提一下 HSR 与 Early-DT,以及通过专利文章,分析当 HSR 遇到 Alpha-Test 时,具体的硬件流程细节究竟是什么样的

    一个常识是:如果片段中做了 clip,也就是 Alpha-Test 的物体,是无法进行提前深度测试(Early-DT)的,而以 HSR 为例,其作为减少 overdraw 的一种类 Early-DT 手段,遇到 Alpha-Test 的物体,必然也会失效。但是问题就在这里,所谓“失效”,流程上会有哪些改变?会带来多大的性能损失?补救方案又是什么?这里网络上还是有不少争议点的,很多篇关于 TBDR 架构的文章,也多多少少会提到这一块

    先放张图,后面可以用作参考,其实这里主要讨论的就是这一块 Defer

    1.2.1 当 HSR 遇到 Alpha-Test

    网上一个主流的说法是:HSR 对于不透明物体(Opaque)并不会关心(或者并不知道)当前的每一个 DrawCall 是否 Alpha-Test,等到 shading 时发现有片段被 clip 了,这时才后知后觉发现原先 HSR 的时候可能会出错,从而对于当前 clip 所在片段的 Tile 直接从头来过,暴力将前面所有的图元(fragment)都跑 ps

    这听上去很恐怖:只要当前 Tile 有 clip 操作发生,整个 HSR 就会退化成同等于完全没有生效: overdraw 的问题当然也不会得到任何改善,也因为这个,Alpha-Test 在很多人心里性能会还不如 Alpha-Blend

    但是这个科学么,其实是不太科学的,做驱动的不会这么暴力,但如果上面的说法没错,那么就应该是丢失了细节?

    后面查阅了更多的资料,甚至还参考了 PowerVR 专利文,大致能得出一个更合理的推测,或者说为上面补充细节

    首先提两个关键信息:

    1. 这个容易被忽略:尽管 HSR 时肯定不知道具体哪些 fragment 会被 clip 以无法确认到底哪个 fragment 是最终可见的,但 HSR 其实是知道每次 DrawCall 有没有开启 Alpha-Test 的
    2. 对于拥有 HSR 专利的 PowerVR,它推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,也就是完全不透明物体全部提交绘制后,再提交所有开了 Alpha-Test 的物体,确实 Unity 也是这么做的

    根据专利论文,中关于 FIG.25 的流程描述,可以得知:

    1. 在 HSR 的过程中,如果完全没有遇到开启了 Alpha-Test 的 DrawCall,那么它就可以顺利的将被挡住的 fragment 信息( Z_AZ_C 全部丢弃,而不被挡住的 fragment(Z_B)会使用 TagBuffer 标记延迟,等到后面统一执行片段着色计算,这对应着上图仅有 Z_AZ_BZ_C 的情况,这非常完美的解决了 overdraw 的问题
    2. 但是当 HSR 时遇到第一个开启 Alpha-Test 的 DrawCall(对应上图中的 Z_D)时,关键点来了:由于此时无法判断最后到底会显示 Z_D 还是 Z_B,所以此时硬件就会将前面 TagBuffer 标记的所有 fragment(Z_B)直接进行 shading(可以理解为直接开始走 pipeline 的下一个流程),同时更新当前 HSR 的信息,并在确保 shading 完之前,HSR 会被 block
    3. 但是 HSR 还没有说因此废掉,如果在此之后收到 Z_E 的 DrawCall,由于 Alpha-Test 的原因一样无法判断最后会显示 Z_E 还是 Z_D,硬件就会把 Z_D丢到 pipeline 的下一步直接进行 shading,到此时硬件就确定了每个 Z_D 的每个 fragment 是否会被 clip,从而深度被确定(这里为什么不丢 Z_E 到下一步先计算,而是丢 Z_D 先计算,主要是由于策略最优:因为 Z_D 你不 shading 深度信息是确定不了的,也无法写入,所以不如丢 Z_D 直接把深度算出来,说不准它把 Z_E 挡住了, Z_E 就可以直接确认丢弃)Alpha-Test 物体的深度确定了,在此之后的 HSR 当然是又会再次开启的(生效的)

    好了,对于最后一个 Z_F,由于他会挡住 Z_D并且已经是最后一个 DrawCall 了,确认在最上面就直接走下一步进行 shading,当此 Tile 下的所有 geometry 的都处理完,合法的片元信息都被送到 pipeline 的后面

    总结一下,对于 ABCDEF 六个 DrawCall,如果都没有开启 Alpha-Test,那么最后会被送到 pipeline 的下一步进行 shading 的只会是最前面的 Z_F(完美情况),而对于实际情况:Z_D开启了 Alpha-Test,那么最后被送到 pipeline 的下一步进行 shading 的就会有 Z_F Z_D Z_B  

    因此可以得出结论:

    1. 开启 Alpha-Test 确实会带来性能问题:明显会打断 HSR,遇到开启了 Alpha-Test 的 DrawCall 时不得不 block HSR 流程:将当前 flush 的 fragment 全部直接 shading,但同理:其也没有很夸张到所有 fragment 都会被 shading,至少在遇到第一个 Alpha-Test 前的所有不透明物体都享受了其优化
    2. 接上,这也很好印证为什么 PowerVR 推荐的渲染顺序是 Opaque → AlphaTest → AlphaBlend,至少你不要穿插着绘制 Alpha-Test 的物体
    3. 具体 Alpha-Test 和 Alpha-Blend 的性能比较,要看你怎么处理 Alpha-Test 的物体,和平台有关系,和当前的场景复杂程度也有关系

    二、软件 DepthPrePass

    2.1 引申:处理 Alpha-Test 物体的更多策略

    除了将 Alpha-Test 物体放在最后面渲染外,还有一种方案就是对其现做一个软件的 Early-DT,也就是 Depth PrePass

    一个经典的例子就是树叶的渲染,出于性能考虑,单片树叶的顶点数不能太多,因此树叶的形状需要用 clip 像素的方式扣出来,而绘制一棵树的时候还会有个比较吃性能的地方:就是树叶在大多数视角方向上,都有大量的 fragment 叠加(用人话说就是一条射线可以穿过无数片树叶),因此在不做 Early-Z 的情况下,一棵树必然会出现大量的 overdraw

    但上面也分析了 HSR 和 Alpha-Test,看上去硬件并没有办法帮我们解决这么多 overdraw 的问题,实际打出的手机包拉近看树叶,确实也出现了掉帧的情况,因此为了减少计算量,Depth PrePass 就再所难免了

    这样做还有一个好处是:对于 Alpha-Test 物体,可以直接在 Depth PrePass 中做 clip 操作,这样在后面绘制的时候,是可以完全当成不透明物体的,不但可以减少大量的片段着色计算,也同等于把 Alpha-Test 的物体也提到了最前面去画,对于上面的流程就是:

    1. 对树叶做 EarlyZ,这一步只写入深度,并且同时做 clip
    2. 再正常绘制树叶,考虑光照和环境,此时它就是不透明物体,无需标记 Alpha-Test 和 clip
    3. 绘制其它不透明物体

    搞定!如果树遮挡了大量地形,后面地形 overdraw 的问题也不会出现

    2.2 部分代码参考

    DepthPrePassRenderFeature.cs:

    1. public class MOpaqueEarlyZRenderFeature : ScriptableRendererFeature
    2. {
    3. RenderObjectsPass _preDepthPass;
    4. RenderObjectsPass _RenderersPass;
    5. public override void Create()
    6. {
    7. string profilerPreDepthTag = "Opaque EarlyZ PreDepth";
    8. string[] preDepthShaderTagIds = {"OpaqueEarlyZPreDepth" };
    9. RenderObjects.CustomCameraSettings cameraSettings = new RenderObjects.CustomCameraSettings();
    10. _preDepthPass = new RenderObjectsPass(profilerPreDepthTag, RenderPassEvent.BeforeRenderingOpaques, preDepthShaderTagIds,
    11. RenderQueueType.Opaque,-1, cameraSettings);
    12. string profilerRenderersPassTag = "Opaque EarlyZ";
    13. string[] renderersPassTagIds = { "OpaqueEarlyZ" };
    14. _RenderersPass = new RenderObjectsPass(profilerRenderersPassTag, RenderPassEvent.BeforeRenderingOpaques, renderersPassTagIds,
    15. RenderQueueType.Opaque, -1, cameraSettings);
    16. }
    17. // Here you can inject one or multiple render passes in the renderer.
    18. // This method is called when setting up the renderer once per-camera.
    19. public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    20. {
    21. renderer.EnqueuePass(_preDepthPass);
    22. renderer.EnqueuePass(_RenderersPass);
    23. }
    24. }

    DepthPrePassShader:

    1. Pass
    2. {
    3. //仅写入深度的 Pass,在这里做好 clip
    4. Tags{ "LightMode" = "OpaqueEarlyZPreDepth" }
    5. Blend One Zero, One Zero
    6. ZTest LEqual
    7. ZWrite On
    8. ColorMask 0
    9. HLSLPROGRAM
    10. #pragma prefer_hlslcc gles
    11. #pragma exclude_renderers d3d11_9x
    12. #pragma target 3.5
    13. #pragma multi_compile_instancing
    14. #pragma vertex DepthOnlyVertex
    15. #pragma fragment DepthOnlyFragment
    16. #define _ALPHATEST_ON
    17. #define SC_OBJECT
    18. #define USE_DISSOLVE_DISTANCE
    19. #include "../Include/SceneDepthOnlyPass.hlsl"
    20. ENDHLSL
    21. }
    22. Pass
    23. {
    24. //实际绘制物体的 Pass
    25. }
    1. half4 DepthOnlyFragment(Varyings input) : SV_TARGET
    2. {
    3. UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    4. #if USE_ALPHATEST
    5. half a = 1.0;
    6. #ifdef _ALPHATEST_ON
    7. a = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a * _Color.a;
    8. a = a - _Cutoff;
    9. #endif
    10. #if USE_DITHER_ALPHATEST_CLIP
    11. half dist = distance(input.worldPos, _WorldSpaceCameraPos);
    12. a = min(a,DitherAlpha(input.scrPos, saturate(dist * 0.2)));
    13. #endif
    14. clip(a);
    15. #endif
    16. #if _Test_OUTPUT
    17. return half4(1.0, 0.0, 0.0, 1.0);
    18. #else
    19. return 0;
    20. #endif
    21. }

  • 相关阅读:
    《Python编程:从入门到实践》第九章练习题
    时间序列常用数据处理
    sqli-labs/Less-51
    python开发之个人微信号的二次开发
    Ansible ad-hoc 临时命令
    python中不可变类型和可变类型
    Java异步注解@Async详解
    Jenkins持续集成部署-配置Harbor机器人账号推送镜像
    webpack5 之 基础构建打包
    【C++智能指针】智能指针的发展和循环引用的原理和解决
  • 原文地址:https://blog.csdn.net/Jaihk662/article/details/127093574