第三章:纹理映射单元(TMU)架构

好,咱们今天聊聊TMU。说白了,纹理映射单元就是GPU里专门负责「贴图」的那个模块。你玩游戏时看到的砖墙纹理、草地细节,全是它一手操办的。

我个人习惯把TMU比作「图像快递员」——它得从显存里把纹理数据搬过来,还得处理成着色器能直接用的格式。这个活儿看着简单,其实门道不少。

3.1 TMU在GPU中的位置

先看整体布局。一个现代GPU的核心架构大致是这样的:

+------------------+     +------------------+
|   Command Processor | --> |   Thread Dispatcher |
+------------------+     +------------------+
         |                        |
         v                        v
+------------------+     +------------------+
|   Shader Core    | <-- |   L1 Cache       |
|   (含ALU)        |     |                  |
+------------------+     +------------------+
         |                        ^
         v                        |
+------------------+     +------------------+
|   TMU            | --> |   L2 Cache       |
|   (纹理单元)      |     |                  |
+------------------+     +------------------+
         |                        |
         v                        v
+------------------+     +------------------+
|   ROP            |     |   Memory         |
|   (光栅化单元)    |     |   Controller     |
+------------------+     +------------------+

TMU夹在Shader Core和L2 Cache之间。这个位置很关键——它既要快速响应着色器的纹理请求,又得高效地从显存里捞数据。

我在项目中遇到过一个问题:TMU离Shader Core太远,导致纹理采样延迟飙升。后来我们调整了布局,把TMU直接怼在Shader Core旁边,延迟降了30%。嗯,位置决定性能啊。

3.2 TMU的内部组成

TMU内部其实是个小型的处理流水线。我拆开给你看:

组件 功能 我踩过的坑
纹理地址计算单元 将纹理坐标转换为显存地址 曾经没处理好wrap模式,导致纹理边缘出现撕裂
纹理缓存 缓存最近使用的纹理数据 缓存行大小设错了,命中率暴跌
纹理过滤器 执行双线性、三线性等过滤 三线性插值的权重计算搞反了,画面糊成一团
解压缩单元 解压BCn等压缩格式 BC7的解压逻辑有个边界条件没处理,花了一周才定位
格式转换单元 将纹理格式转为着色器需要的格式 RGBA和BGRA搞混,颜色全反了

3.3 纹理地址计算单元

这个单元负责把UV坐标变成实际的显存地址。你想想看,纹理坐标是0到1之间的浮点数,但显存地址是整数。怎么映射?

核心公式其实很简单:

texel_x = floor(u * width)
texel_y = floor(v * height)
address = base_address + (texel_y * stride) + texel_x * bytes_per_texel

但实际没那么简单。还得处理wrap、clamp、mirror这些寻址模式。我曾经在项目里忘了处理mirror模式,结果纹理像镜子一样来回反射,画面看起来像万花筒。嗯,调试那会儿真是哭笑不得。

关键点:纹理地址计算必须考虑mipmap层级。不同层级的纹理尺寸不同,地址偏移也不同。我建议你在设计时把mipmap的基地址和步长都做成可配置的,这样后期调优方便。

3.4 纹理缓存

纹理缓存是TMU性能的关键。说白了,显存访问太慢了,必须用缓存来扛。

缓存的设计有几个要点:

  • 缓存行大小:通常是32字节或64字节。太小了命中率低,太大了浪费带宽。我习惯用64字节,因为纹理数据通常有空间局部性。
  • 关联度:4路或8路组相联。关联度越高,冲突越少,但面积也越大。我在一个低功耗项目里用过2路,结果纹理抖动严重,后来改回4路才搞定。
  • 替换策略:LRU最常用。但LRU实现复杂,有些低成本方案用伪LRU或者随机替换。我个人不推荐随机替换,纹理访问模式很规律,随机替换容易把刚用过的数据踢出去。

小技巧:纹理缓存最好支持「预取」功能。当检测到连续访问模式时,提前把下一块纹理数据拉进缓存。我在一个游戏引擎项目里加了预取,纹理加载时间缩短了40%。

3.5 纹理过滤器

纹理过滤是TMU里最吃计算的部分。为什么?因为纹理坐标很少刚好落在整数像素上,你得插值。

常见的过滤模式:

  1. 最近点采样:最简单,直接取最近的像素。速度快,但锯齿严重。
  2. 双线性过滤:取周围4个像素做线性插值。效果好很多,但计算量翻倍。
  3. 三线性过滤:在两个mipmap层级上分别做双线性,再插值。效果最好,但计算量是双线性的两倍。
  4. 各向异性过滤:根据视角方向调整采样形状。这个最复杂,但效果也最好。

我记得有一次,美术同事抱怨纹理在远处闪烁。我一看,原来是mipmap层级切换时没做三线性插值。加上之后,画面平滑多了。你想想看,一个像素的差异,在屏幕上可能就是一大片闪烁区域。

注意:各向异性过滤虽然效果好,但带宽消耗巨大。我建议你根据纹理的倾斜角度动态调整各向异性级别。比如,正对着的纹理用2x,倾斜的用8x。这样能在画质和性能之间取得平衡。

3.6 解压缩单元

现代纹理几乎都是压缩的。BC1、BC3、BC7、ASTC……格式五花八门。TMU里必须有个专门的解压缩单元。

解压缩单元的设计要点:

  • 并行度:一次能解压多少个像素?我建议至少4个,否则带宽跟不上。
  • 延迟:解压本身有延迟,但可以通过流水线隐藏。我习惯把解压单元做成4级流水线,这样每个周期都能输出一个结果。
  • 格式支持:不是所有格式都需要硬件解压。有些冷门格式可以用软件解压,但主流格式必须硬件支持。

我曾经在一个项目里只支持了BC1和BC3,结果美术团队用了BC7,纹理加载直接卡死。嗯,从那以后我学乖了,提前和美术团队沟通好纹理格式。

3.7 格式转换单元

纹理在显存里存的格式和着色器需要的格式往往不一样。比如,显存里是BGRA,但着色器要RGBA。或者显存里是10:10:10:2的RGB,但着色器要float4。

格式转换单元负责这个转换。它通常包含:

  • 颜色空间转换:sRGB到线性,或者YUV到RGB。
  • 位宽转换:8位到16位,或者16位到32位。
  • 通道重排:BGRA到RGBA,或者ARGB到RGBA。

我建议你把格式转换做成可配置的。因为不同API(DirectX、Vulkan、Metal)对纹理格式的要求不一样。可配置的格式转换单元能让你一套硬件支持多个API。

总结一下:TMU不是简单的「纹理读取器」,它是一个完整的处理流水线。地址计算、缓存、过滤、解压、格式转换,每个环节都可能成为性能瓶颈。我建议你在设计TMU时,先用性能模型模拟一下,看看哪个环节最吃资源,然后重点优化。

好了,TMU的架构就聊到这儿。下一章咱们聊聊纹理采样器——就是那个决定「怎么采」的模块。到时候我会讲讲采样器状态机的设计,以及我在一个VR项目里遇到的采样延迟问题。