3、顶点处理单元(VU):顶点着色器硬件架构

好,咱们今天聊聊顶点处理单元。说白了,就是GPU里负责处理每个顶点的那部分硬件。你想想看,一个3D模型少说几千个顶点,多的上百万,每个顶点都要经过坐标变换、光照计算、蒙皮等一系列操作。这个活儿,就是顶点着色器干的。

我个人习惯把顶点处理单元看作整个渲染管线的“第一道工序”。如果这道工序出了问题,后面再好的像素着色器也白搭。我在项目中遇到过好几次,因为顶点处理瓶颈导致整个帧率上不去,最后发现是顶点缓存命中率太低——嗯,这个咱们后面细说。

3.1 顶点着色器硬件架构

顶点着色器的硬件实现,说白了就是一个高度并行的SIMD(单指令多数据)处理阵列。每个顶点被分配到一个处理单元上,执行相同的着色器程序,但处理不同的顶点数据。

为什么用SIMD?因为顶点之间是相互独立的。你处理顶点A和处理顶点B,不需要知道对方的存在。这种天然的并行性,正是SIMD最擅长的场景。

核心架构组件:

  • 顶点获取单元(Vertex Fetch):从显存中读取顶点属性数据
  • 着色器核心阵列(Shader Core Array):执行顶点着色器程序的ALU集群
  • 顶点缓存(Vertex Cache):缓存最近处理过的顶点数据
  • 图元装配单元(Primitive Assembly):将顶点组装成三角形/线段

我记得有一次调试一个性能问题,发现顶点获取单元成了瓶颈。原因很简单——顶点数据在显存里是稀疏存储的,每次读取都要跳来跳去,缓存根本帮不上忙。后来我们重新排布了顶点数据的布局,把连续顶点放在连续地址里,性能直接提升了30%。

3.2 统一着色器架构 vs 分离架构

这个争论在GPU发展史上可是个大话题。我刚开始做GPU设计那会儿,主流还是分离架构——顶点着色器和像素着色器各管各的,硬件资源互不共享。

对比维度 分离架构 统一着色器架构
资源利用率 低(负载不均衡时浪费严重) 高(动态分配)
硬件复杂度 低(各单元独立设计) 高(需要调度逻辑)
编程灵活性 差(固定管线) 好(统一编程模型)
典型代表 早期GPU(如GeForce 6系列) 现代GPU(如G80之后)

分离架构有个很尴尬的问题:如果场景里顶点特别多但像素很少(比如线框渲染),顶点着色器忙得要死,像素着色器却在闲着。反过来,如果场景里像素特别多(比如全屏特效),像素着色器又成了瓶颈。说白了,就是资源没法互相借用。

统一着色器架构的出现,彻底解决了这个问题。所有着色器核心都是一样的,可以执行顶点程序、像素程序、甚至计算程序。调度器根据负载情况动态分配核心资源。你想想看,这就像把原来两个独立的工厂合并成一个,工人可以随时调配到最忙的生产线上。

我的经验:统一架构虽然好,但调度器的设计非常关键。我曾经见过一个设计,调度器为了追求公平性,频繁地在不同任务之间切换,结果上下文切换的开销把性能优势全吃掉了。后来我们改成了“尽量让一个核心连续处理同一类任务”的策略,效果好了很多。

3.3 顶点缓存与重用策略

顶点缓存,这是顶点处理单元里最容易忽视但又最关键的部分之一。为什么?因为顶点重用太常见了。

你想想看,一个三角形网格里,每个顶点平均被6个三角形共享。如果不做缓存,同一个顶点要被处理6次。做了缓存,第一次处理完存起来,后面5次直接命中,省了5/6的计算量。

顶点缓存通常分为两级:

  • L0缓存(顶点着色器内部):容量小(通常几十个顶点),延迟极低(1-2个周期)
  • L1缓存(共享缓存):容量大(几百到上千个顶点),延迟稍高(几个周期)

缓存的重用策略,我见过几种:

  1. 最近最少使用(LRU):最常用,效果稳定
  2. 先进先出(FIFO):实现简单,但命中率不如LRU
  3. 基于图元顺序的预取:根据三角形遍历顺序,提前加载可能用到的顶点

避坑指南:我曾经犯过一个错误——把顶点缓存做得太大。觉得缓存越大命中率越高嘛,结果发现延迟反而上去了。因为大缓存需要更多的比较逻辑和更长的寻址时间。后来我学乖了,缓存大小要根据实际工作集来定,不是越大越好。

还有一个容易被忽略的点:顶点缓存的索引方式。如果用顶点ID做索引,那很简单,但顶点ID可能不连续。如果用哈希表,实现复杂但更灵活。我个人习惯用直接映射缓存,配合顶点ID的低位做索引,高位做标签。这样实现简单,命中率也够用。

嗯,顶点处理单元就聊这么多。记住一句话:顶点处理是渲染管线的起点,起点慢了,后面再快也白搭。下一章咱们聊聊图元装配和光栅化,那又是另一番天地了。