当前位置: 首页 > news >正文

TBR架构的Tiling Pass解析

开工之前的分拣工作


从一个快递分拣中心说起

双十一。

一个快递分拣中心收到了100万个包裹。每个包裹上贴着收件地址。这些包裹要送到全市2040个小区。

有两种分拣方式。

方式一:不分拣。快递员拿起一个包裹,看地址,开车送过去。回来再拿一个,看地址,开车送过去。100万个包裹,100万次出车。每次出车可能去不同的小区。今天去东边,下一趟去西边,再下一趟又回东边。

方式二:先分拣。在仓库里摆2040个筐,每个筐对应一个小区。100万个包裹,一个一个看地址,扔到对应的筐里。分拣完毕后,快递员拿起第一个筐,一次性把这个筐里的所有包裹送到对应小区。然后拿第二个筐。2040个筐,2040次出车。每次出车只去一个小区,把那个小区的所有包裹一次送完。

方式一是IMR。方式二是TBR。

分拣的过程,就是Tiling Pass。


一、Tiling Pass在做什么?

主场景渲染开始之前,GPU要先做一件事:把所有三角形分配到它们覆盖的Tile里。

屏幕被切成一个个小方块(Tile),通常是16×16或32×32像素。1920×1080的屏幕,按32×32切分,得到60×34 = 2040个Tile。

场景里有几万个三角形。每个三角形经过顶点着色器变换后,在屏幕上占据一定的区域。这个区域可能覆盖一个Tile,也可能覆盖几十个、几百个Tile。

Tiling Pass的任务:对每个三角形,算出它覆盖了哪些Tile,然后把这个三角形的引用添加到那些Tile的列表里。

输入:所有三角形的屏幕空间坐标 输出:每个Tile的三角形列表 Tile (0,0): [三角形 5, 三角形 23, 三角形 107, ...] Tile (1,0): [三角形 5, 三角形 24, 三角形 108, ...] Tile (2,0): [三角形 23, 三角形 24, 三角形 55, ...] ... Tile (59,33): [三角形 892, 三角形 1024, ...]

这个过程完成后,GPU就知道了:每个Tile里有哪些三角形需要画。

然后GPU一个Tile一个Tile地渲染。处理Tile (0,0)的时候,只画Tile (0,0)列表里的三角形。处理Tile (1,0)的时候,只画Tile (1,0)列表里的三角形。

Tiling Pass是TBR架构的第一步。没有它,后面的逐Tile渲染就无从谈起。


二、Tiling Pass的详细流程

2.1 第一步:顶点着色

Tiling Pass开始之前,所有三角形的顶点要先经过顶点着色器。

模型空间坐标 → 世界空间坐标 → 观察空间坐标 → 裁剪空间坐标 → NDC坐标 → 屏幕空间坐标

顶点着色器把每个顶点从3D世界变换到2D屏幕上。变换完之后,每个三角形有三个屏幕空间坐标。

三角形A的三个顶点: V0 = (120.5, 340.2) V1 = (890.3, 120.8) V2 = (450.7, 780.1)

注意:顶点着色器在Tiling Pass之前就执行了。Tiling Pass拿到的是已经变换好的屏幕空间坐标。

有些资料把顶点着色也算作Tiling Pass的一部分。有些把它算作独立的阶段。这不重要。重要的是:Tiling Pass需要屏幕空间坐标才能工作。

2.2 第二步:计算包围盒

对每个三角形,计算它在屏幕上的轴对齐包围盒(AABB)

三角形A: V0 = (120.5, 120.8) ← 取三个顶点的x最小值和y最小值 V1 = (890.3, 780.1) ← 取三个顶点的x最大值和y最大值 包围盒: x_min = 120.5, x_max = 890.3 y_min = 120.8, y_max = 780.1

为什么要算包围盒?因为判断一个三角形覆盖了哪些Tile,最快的方法是先用包围盒粗略判断,再精确判断。

2.3 第三步:包围盒映射到Tile坐标

把包围盒的像素坐标转换成Tile坐标。

Tile大小 = 32×32 三角形A的包围盒: x_min = 120.5 → Tile_x_min = floor(120.5 / 32) = 3 x_max = 890.3 → Tile_x_max = floor(890.3 / 32) = 27 y_min = 120.8 → Tile_y_min = floor(120.8 / 32) = 3 y_max = 780.1 → Tile_y_max = floor(780.1 / 32) = 24 三角形A覆盖的Tile范围: x方向:第3列到第27列 = 25列 y方向:第3行到第24行 = 22行 总共:25 × 22 = 550个Tile

一个三角形覆盖了550个Tile。这个三角形的引用要添加到550个Tile的列表里。

2.4 第四步:精确裁剪(可选)

包围盒是粗略的。三角形是斜的,包围盒是方的。包围盒的角落里可能有些Tile其实不被三角形覆盖。

┌─────────────────────┐ │ ╱ │ ← 包围盒的右上角 │╱ 三角形 │ 三角形没有覆盖这里 │╲ │ 但包围盒覆盖了 │ ╲ │ │ ╲ │ └─────────────────────┘

精确裁剪会进一步判断:包围盒范围内的每个Tile,是否真的被三角形覆盖。

怎么判断?用三角形的三条边做半平面测试。一个Tile的四个角点,如果都在三角形的某条边的外侧,那这个Tile就不被三角形覆盖。

精确裁剪能减少无效的Tile分配,但本身也有计算开销。大多数GPU会做一定程度的精确裁剪,但不会做到像素级别的精确——那就变成光栅化了,不是Tiling Pass该做的事。

2.5 第五步:写入Tile列表

对每个被覆盖的Tile,把三角形的引用(通常是一个索引或指针)添加到那个Tile的三角形列表里。

Tile (5, 7) 的列表: [三角形12] → [三角形12, 三角形A] → [三角形12, 三角形A, 三角形89] → ...

这个列表存在哪里?主内存。

片上内存太小,存不下所有Tile的所有三角形列表。2040个Tile,每个Tile可能有几十到几百个三角形。总数据量可能有几MB到几十MB。

Tiling Pass的输出——Tile列表——存在主内存里。后续逐Tile渲染的时候,GPU从主内存读取对应Tile的三角形列表,加载到片上内存,然后开始渲染。


三、Tiling Pass的开销分析

Tiling Pass不是免费的。它有三种开销。

3.1 计算开销

每个三角形都要:

  • 计算包围盒(几次比较运算)
  • 映射到Tile坐标(几次除法)
  • 可能的精确裁剪(半平面测试)
  • 写入Tile列表(内存写入)

场景里有10万个三角形。每个三角形做一次上述操作。

计算量不大。跟片段着色器的纹理采样和光照计算比起来,Tiling Pass的计算量微不足道。几次整数运算和比较运算,GPU几个时钟周期就搞定了。

Tiling Pass的计算开销通常在0.1-0.3ms。

3.2 带宽开销

Tile列表要写入主内存。

每个三角形引用占多少空间?通常4-8字节(一个32位或64位的索引/指针)。

一个三角形平均覆盖多少个Tile?这取决于三角形的大小。

小三角形(几个像素大):覆盖1-2个Tile 中等三角形(几百个像素):覆盖5-20个Tile 大三角形(几千个像素):覆盖几十到几百个Tile 全屏三角形:覆盖所有2040个Tile

假设场景有10万个三角形,平均每个覆盖10个Tile。

总Tile引用数 = 10万 × 10 = 100万 每个引用8字节 总数据量 = 100万 × 8 = 8MB

8MB写入主内存。在移动端30-50GB/s的带宽下,这不算多,但也不是零。

3.3 内存开销

Tile列表本身占内存。

100万个引用 × 8字节 = 8MB。

加上每个Tile的列表头信息(起始地址、长度等),总共可能10-15MB。

在内存紧张的移动端,10-15MB不是小数目。

而且Tile列表的大小是动态的。场景简单的时候可能只有2MB,场景复杂的时候可能有20MB。GPU需要预分配足够大的缓冲区来存放Tile列表,或者用动态分配的方式。


四、大三角形的噩梦

Tiling Pass最头疼的问题是大三角形

一个覆盖整个屏幕的三角形(比如天空盒的一个面、全屏后处理的三角形),会被分配到所有2040个Tile里。

一个全屏三角形: 覆盖2040个Tile 写入2040个Tile引用 = 2040 × 8字节 = 16KB

16KB不多。但如果有100个大三角形呢?

100个大三角形 × 2040个Tile × 8字节 = 1.6MB

而且问题不只是带宽。每个Tile在渲染的时候,都要处理这100个大三角形。

Tile (0,0) 的三角形列表里有这100个大三角形。Tile (0,1) 的列表里也有。Tile (59,33) 的列表里也有。

每个Tile都要对这100个大三角形做光栅化(虽然只处理Tile内的部分)。

100个大三角形 × 2040个Tile = 204000次光栅化操作。

如果这100个大三角形只是普通大小的三角形,每个覆盖10个Tile,那只有100 × 10 = 1000次光栅化操作。

大三角形让光栅化的工作量膨胀了200倍。

4.1 解决方案:三角形裁剪

在Tiling Pass之前,对大三角形做裁剪。把一个大三角形切成多个小三角形,每个小三角形只覆盖少量Tile。

一个全屏三角形 → 裁剪成64个小三角形 每个小三角形覆盖约32个Tile 总Tile引用 = 64 × 32 = 2048(跟原来的2040差不多)

裁剪没有减少Tile引用的总数,但减少了每个Tile列表的长度。因为每个小三角形只出现在少量Tile的列表里,而不是所有Tile的列表里。

但裁剪本身有开销。而且裁剪会增加三角形的数量,增加顶点着色器的工作量。

4.2 解决方案:分层Tiling

有些GPU用分层的Tile结构。

第一层:大Tile(256×256像素) 1920×1080 → 8×5 = 40个大Tile 第二层:中Tile(64×64像素) 每个大Tile包含4×4 = 16个中Tile 第三层:小Tile(16×16像素) 每个中Tile包含4×4 = 16个小Tile

大三角形只分配到第一层的大Tile里。小三角形直接分配到第三层的小Tile里。中等三角形分配到第二层。

大三角形只需要写入40个大Tile的列表,而不是2040个小Tile的列表。

渲染的时候,GPU先处理大Tile,再细分到中Tile,再细分到小Tile。大三角形在大Tile级别就被处理了,不需要在每个小Tile里重复处理。


五、小三角形的另一个问题

大三角形让Tiling Pass的输出膨胀。小三角形让Tiling Pass的输入膨胀。

现代游戏的模型越来越精细。一个角色可能有10万个三角形。很多三角形在屏幕上只有几个像素大,甚至小于一个像素。

小于一个像素的三角形叫做微三角形(Micro Triangle)。

微三角形对Tiling Pass来说是纯粹的浪费。它覆盖一个Tile(甚至不覆盖任何像素),但Tiling Pass还是要处理它:计算包围盒、映射Tile坐标、写入Tile列表。

10万个微三角形,每个只覆盖1个Tile。Tiling Pass要处理10万次,但最终只产生10万个Tile引用。如果这10万个三角形合并成1000个大一点的三角形,Tiling Pass只需要处理1000次。

微三角形让Tiling Pass的效率急剧下降。

5.1 解决方案:LOD

远处的物体用低模。减少三角形数量。减少微三角形。

5.2 解决方案:Mesh Shader

Mesh Shader是现代GPU的新特性。它允许在GPU上动态生成和剔除三角形。可以在Mesh Shader里把微三角形合并成更大的三角形,或者直接剔除不可见的三角形。

在Tiling Pass之前就减少三角形数量。

5.3 解决方案:Nanite的做法

虚幻5的Nanite系统在GPU上做了一套完整的LOD选择和三角形剔除。它保证送到光栅化阶段的三角形大小适中——不会太大(避免Tile列表膨胀),也不会太小(避免微三角形浪费)。

Nanite本质上是在帮Tiling Pass减负。


六、Tiling Pass的输出长什么样?

让我画一个具体的例子。

假设屏幕只有8×8像素,Tile大小是4×4。所以有2×2 = 4个Tile。

场景里有5个三角形:A、B、C、D、E。

屏幕(8×8像素,4个Tile): ┌───────┬───────┐ │Tile │Tile │ │(0,0) │(1,0) │ │ A,B │ B,C │ │ │ │ ├───────┼───────┤ │Tile │Tile │ │(0,1) │(1,1) │ │ A,D │ C,D,E│ │ │ │ └───────┴───────┘ 三角形覆盖情况: A:覆盖Tile(0,0)和Tile(0,1) → 左边两个 B:覆盖Tile(0,0)和Tile(1,0) → 上面两个 C:覆盖Tile(1,0)和Tile(1,1) → 右边两个 D:覆盖Tile(0,1)和Tile(1,1) → 下面两个 E:只覆盖Tile(1,1) → 右下角

Tiling Pass的输出:

Tile (0,0) 的三角形列表:[A, B] → 2个三角形 Tile (1,0) 的三角形列表:[B, C] → 2个三角形 Tile (0,1) 的三角形列表:[A, D] → 2个三角形 Tile (1,1) 的三角形列表:[C, D, E] → 3个三角形

在主内存中的存储方式:

┌─────────────────────────────────────────┐ │ Tile列表头(每个Tile的起始位置和长度) │ │ │ │ Tile(0,0): offset=0, count=2 │ │ Tile(1,0): offset=2, count=2 │ │ Tile(0,1): offset=4, count=2 │ │ Tile(1,1): offset=6, count=3 │ ├─────────────────────────────────────────┤ │ 三角形引用数组 │ │ │ │ [0]: A │ │ [1]: B │ │ [2]: B ← B出现了两次(覆盖两个Tile) │ │ [3]: C │ │ [4]: A ← A也出现了两次 │ │ [5]: D │ │ [6]: C ← C也出现了两次 │ │ [7]: D ← D也出现了两次 │ │ [8]: E │ └─────────────────────────────────────────┘

5个三角形,产生了9个引用。因为有些三角形覆盖了多个Tile,它们的引用出现了多次。

引用总数 ≥ 三角形总数。覆盖越多Tile的三角形,引用越多。


七、Tiling Pass对开发者的影响

7.1 你看不到Tiling Pass

Tiling Pass是GPU硬件自动完成的。你不需要写任何代码来触发它。你提交Draw Call,GPU自动在渲染之前做Tiling。

你在Vulkan/Metal的API里看不到任何跟Tiling相关的函数调用。

但你能感受到它的存在。当你的场景有大量三角形的时候,即使片段着色器很简单,帧率也可能下降。因为Tiling Pass本身成了瓶颈。

7.2 你能影响Tiling Pass的效率

虽然你不能直接控制Tiling Pass,但你的渲染决策会影响它的效率。

减少三角形数量。用LOD。用遮挡剔除。用视锥体剔除。送到GPU的三角形越少,Tiling Pass越快。

避免极大的三角形。全屏的后处理三角形、天空盒的大面片,都会让Tile列表膨胀。如果可能,把大三角形拆成小的。

避免极小的三角形。微三角形浪费Tiling Pass的处理能力。用LOD确保屏幕上的三角形大小适中。

理想的三角形大小:覆盖几个到几十个像素。不要太大(Tile列表膨胀),不要太小(处理浪费)。

7.3 RenderPass的设计

每次开始一个新的RenderPass,GPU都要重新做一次Tiling Pass(如果这个Pass里有3D几何体的话)。

两个RenderPass = 两次Tiling Pass。

如果你把主场景拆成两个Pass(比如先画不透明物体,再画半透明物体),GPU要做两次Tiling。

尽量把所有几何体放在同一个RenderPass里。用Subpass来分阶段处理,而不是用多个RenderPass。Subpass不会触发新的Tiling Pass。


最后

Tiling Pass是TBR架构的基石。没有它,就没有逐Tile渲染。没有逐Tile渲染,就没有片上内存的优势。没有片上内存的优势,移动端GPU就跟PC GPU一样费电,手机就变成了暖手宝。

Tiling Pass本身不产生任何可见的像素。它不画任何东西。它只是在分拣。把三角形分到对应的Tile里。

但正是这个看不见的分拣工作,让后面的渲染能够在小小的片上内存里高效完成。

就像快递分拣中心。分拣员不送一个包裹。但没有分拣员,快递员就要满城乱跑。

Tiling Pass是那个不送包裹的分拣员。

它不画画。但没有它,画就画不好。

http://www.jsqmd.com/news/530206/

相关文章:

  • Qwen3系统运维手册:Linux服务器部署与监控实战
  • 如何让ThinkPad商务本焕发新生?OpenCore引导技术带来的黑苹果体验革命
  • 《智能体设计模式》第五章精读|工具模式(Tool Pattern)—— 让AI从“语言模型”变成“能干活的智能体”
  • 人类科技的底层任务,本质上都是在验证“空间场本源论
  • 深入SPDK vhost轮询机制:为什么它比传统virtio快3倍?
  • SeqGPT-560M开源大模型教程:免训练、免标注、免微调的NLP新范式
  • 汽车金融风控岗扣子的月度提升计划。复习贷后监控体系和概念。
  • NumPy 函数手册:数组重复与扩展
  • OpenClaw 中文文档 — WhatsApp 与 Telegram 接入
  • 光伏MPPT之变步长电导增量法探究
  • 魔兽争霸III现代系统兼容解决方案与优化指南
  • OpenClaw 中文文档 — v2026.3.23 稳定性修复分析:Auth 系统、浏览器连接与插件生态
  • 全国30米分辨率地形坡度数据Tif格式
  • iOS系统降级与硬件漏洞利用实战指南:基于checkm8技术的设备降级全流程
  • 探索任意极槽数永磁同步电机绕组计算器
  • 某软件验证思路
  • 基础算法:前缀和(Prefix Sum)
  • AssetStudio:3步快速掌握Unity资源提取与管理的终极指南
  • 小米手表表盘设计完整指南:如何用可视化工具10分钟打造个性化界面
  • 掌握Icarus Verilog:从零开始的数字电路仿真完整指南
  • Day22:RAG 王炸进阶!多格式文档 (PDF_Word)+ 多文档知识库搭建
  • 跨平台键鼠共享:3步实现多设备无缝控制
  • python社区智慧医疗养老系统vue3
  • PolSARpro v6.0 (Biomass Edition)安装指南:从依赖配置到环境搭建
  • 回调函数到底算哪一层的?——嵌入式分层设计里最纠结的问题
  • 动画制作行业变革:HY-Motion推动文生动作商业化落地
  • 基于Matlab的信号处理GUI人机交互探索
  • 小白友好!造相-Z-Image极简部署,无需网络也能玩转AI绘画
  • 鸣潮自动化工具ok-ww深度评测:图像识别驱动的游戏效率革新
  • PP-DocLayoutV3入门指南:Gradio界面各组件功能详解与交互逻辑说明