找回密码
 立即注册
查看: 414|回复: 4

GPU图形微架构学习笔记(四)贴图

[复制链接]
发表于 2022-1-23 09:47 | 显示全部楼层 |阅读模式
其余文章参见系列目录

GPU的贴图模块绝非一个简单的数组查找器,里面蕴含了T-OPs级别的计算量。他接受着色器核心(Shader Core)的贴图访问请求,计算出需要获取数据的地址后从显存读取数据,最后将数据进行过滤计算(Filter),结果返回给着色器核心。由于涉及显存读取,所以对带宽的优化也很重要,贴图模块也会设计特殊的Cache和解压缩模块。


贴图模块分为地址计算和数据过滤两部分,早先由于贴图坐标和数据都位于[0,1]区间(经过归一化处理),所以地址模块和数据过滤都可以用定点数计算实现,但是现代图形API需要GPU支持没有范围限制的浮点贴图,所以数据过滤模块也为这种情况实现了浮点计算。
贴图计算

首先我们来介绍下贴图模块所要实现的计算任务。这和贴图采样(Texture Sampler)所设置的过滤模式有关。不同的过滤模式需要获取的贴图像素数量和计算量不同。
不过滤模式

如果设置为不过滤,那只需要获取采样点落到的贴图像素然后返回该像素的数值就行。如图一所示,深红点为采样点,他落到了浅红色标记的贴图像素中,所以我们只需要读取该像素就行。



图一:不过滤贴图采样

线性过滤模式

如果设置为线性过滤,而且贴图没有开启mipmap,那么就是双线性插值,需要获取包围采样点的四个贴图像素,进行三次线性插值。如图二所示,读取包围采样点s的四个像素(t1, t2, t3, t4)。 是s在(t1, t2, t3, t4)围成的正方形(边长为1)里纵横方向的坐标。我们定义线性插值操作:


得到采样点的双线性插值计算为:


所以双线性插值需要三次线性插值计算。



图二:非mipmap线性过滤

mipmap线性过滤模式

我们先从mipmap的目的说起。mipmap为的是解决贴图的minification问题,也就是屏幕空间一个像素覆盖了很多个贴图空间像素的时候。如图三[1]所示,蓝色的像素点比较理想,覆盖的贴图像素和屏幕像素差不多大。但是红色的像素点在该贴图上就有minification问题。



图三:屏幕坐标空间和贴图坐标空间

这时候为了抗锯齿,需要将采样点覆盖的多个贴图像素求平均(滤除高频信号)。最直接的办法就是每次采样获取屏幕像素覆盖的所有贴图像素后求平均,但是这样的话每次采样的计算量取决于覆盖贴图像素数量,变化很大,而且计算量也不小。所以就有了mipmap,一个事先做好求平均的固定计算量算法:
生成贴图时计算一系列分辨率逐级对半的更小贴图,高一级贴图的一个像素是低一级贴图四个像素的平均,这样屏幕空间一个像素在更小的贴图上就会覆盖更少的贴图像素。理想状态就是我们能够在这一系列的贴图中找到一个贴图,一个屏幕像素刚好覆盖一个该贴图像素。但是大多数时候没有这么巧,这些事先对半缩小的贴图中会有一个比屏幕像素稍大一些的,还会有一个比屏幕像素稍小一些的,所以需要分别在这两个贴图上做双线性插值,然后对结果再进行一次线性插值,就是所谓的三线性插值。
那么如何定量地计算出那两个“稍大”和“稍小”的贴图以及做最后那步线性插值呢?这需要引入一个深度层次d的变量,定义如图四[1]所示。他的计算思路是:设屏幕像素间隔为1,先用勾股定理算出贴图中采样点位置的间隔L(这里我们取x、y两个方向上的最大者),最后计算要多少次“对折”( )才能把L变成1,也就是屏幕像素大小和贴图大小相等的理想状态。公式上d是一个连续的数值,对他向上、向下取整就是那两个“稍大”和“稍小”的mipmap level。



图四:mipmap层次d的计算

有了d我们就可以给出三线性插值的具体计算过程了:

  • 对d向下取整获得mipmap level ,采样点在该贴图上做双线性插值得到结果v1
  • 对d向上取整获得mipmap level ,采样点在该贴图上做双线性插值得到结果v2
  • 等于d的小数部分,那么最后采样点的数值
所以三线性插值的计算需要读取8个贴图像素,做7次线性插值。
各向异性过滤模式

各向异性过滤[2][3]针对的是屏幕空间像素在X和Y方向上覆盖的贴图像素差距很大的情况,也就是图四右侧的浅红色方块很细长的情况。公式上看就是计算L的max操作的两个操作数差距大。这时我们只在长边方向存在minification的情况,短边正常或者只有少量minification。
这时候过滤的思路就是在mipmap中选取那个像素大小和短边接近的level,然后在长边方向上选取几个像素求平均。具体的算法允许各家GPU厂商自己实现,OpenGL标准里给出的是参考算法:


其中 就是长边, 就是短边。N是长边比短边的倍数,代表各向异性的程度,也代表需要在长边上对多少像素做平均,而且我们会设置一个最大的maxAniso(一般不超过16),来限制做平均像素的数量。d在该定义下 ,就是为了选取接近短边的mipmap level。最后s值的计算取决于 的大小,为的是在长边方向多采样,  是过滤函数,代表在 变量下采取的计算,可以是之前的不过滤、线性过滤和mipmap线性过滤的一种,取决于GPU实现,最后对分布于长边方向的 过滤结果求平均。
所以各向异性过滤的贴图像素读取数量和计算量取决于N和选择的  ,但是肯定高于之前各向同性的过滤模式。
贴图Cache

讨论贴图模块的Cache[4]前我们先来看看贴图模块需要的内存带宽。现代GPU至少需要G级别的像素填充率,所以匹配的贴图获取率也应该是G级别。比如NVIDIA的GTX1080的贴图获取率大约300G/s[1]。如果每次贴图获取采用的是三线性插值(获取8个贴图像素),贴图像素为32bit,那么需要的带宽是:


所以GPU需要设计高效的Cache来满足这么大带宽。CPU的Cache有效性来源于:

  • 时间相关性:同一个数据会在不久后再次被访问
  • 空间相关性:相邻的数据会被逐个访问
而贴图Cache的有效性主要来自空间相关性,很少来自时间相关性。因为一个GPU线程很少会去两次访问相同的贴图像素,而相邻的屏幕像素线程很可能访问相邻的贴图像素。所以为了更好的满足空间相关性,贴图数据需要分块存储,比如8x8或者16x16的块,放进贴图cache的一个line里。这样相邻的屏幕像素线程的贴图读取就很可能都落在同一个cache line里,如图五[1]所示。即使是单线程的访问,一次获取一个2x2的小块,如果是线性存储的话两行的贴图至少也需要两个cache line。



图五:相邻屏幕像素的贴图采样

分块的另一个好处是抗采样方向变化的能力。如图六所示[1],如果是线性存储,采样方向顺着X轴方向时,cache line的使用率是高的,但是如果采样方向顺着Y轴,每一行都至少一个cache line,而且cache line里很多还是无关数据,利用率不高。如果分块存储,无论采样方向怎么变,cache性能的变化也不会很大。



图六:Y方向贴图访问

我们在光栅化一文中提到的分块遍历会将一个块内的屏幕像素线程编成一组一起执行,也有强化空间相关性、提高贴图cache性能的作用。如图七[1]所示,右侧三角形的屏幕像素点是一个2x2的块,这四个采样点相比于图六4x1的采样点需要更少的cache line。



图七:分块遍历时的贴图Cache使用

贴图压缩

GPU贴图压缩算法的选择和帧缓冲模块相同的地方是都需要支持随机访问。不同的地方是GPU对贴图是只读访问,贴图压缩可以事先做好(得益于此可以找到很多给开发者的文档,不像帧缓冲压缩算法那么封闭),GPU只需要实现解压缩就行了。所以贴图压缩算法可以压缩部分复杂一些,但解压部分必须简单。而且由于不会频繁写入造成失真累积,所以贴图压缩算法允许失真。
贴图压缩有基于块的压缩算法,比如S3TC[5]、ETC[6]和ASTC[7],也有不基于块的压缩算法比如PVRTC[8]。这里介绍一种比较简单的贴图压缩算法——S3TC压缩。帧缓冲操作中提到过无损的调色盘算法,S3TC则是一种有损的调色盘算法:

  • 调色盘只有四个颜色,前两个颜色C0、C1各16bit表示(RGB565),后两个颜色是前两个颜色的固定线性插值,不需要存储空间:




  • 4x4的色块,每2bit表示一个像素的调色盘索引
所以总共需要:


对每像素24bit的贴图来说压缩比就是6:1。
S3TC压缩时可以理解为RGB颜色空间里一条直线上四个等距点对于16个点的最小二乘拟合问题。所以压缩过程相比于解压过程的查表要复杂得多。
隐藏访问延迟

着色器核心获取贴图是一个很长的过程,包括计算地址、读取内存、解压缩、过滤。这往往需要几百个时钟周期的等待。早期GPU一般通过预取——提前获取周围贴图——来减少访问延迟。但是现代GPU采取的办法是“隐藏”而不是“减少”,从ILP的思路转向TLP。
现代GPU的着色器核心会有一个很大的线程池,里面储备了很多等待执行的线程组。由于每个计算单元每次只能执行其中的一组,所以一旦正在执行的线程组需要获取贴图就被挂起放回线程池等待结果返回,而计算单元马上切换到其他池子中的可执行线程组就行了。
所以虽然单个线程组的贴图访问时间没有减少,但由于线程组够多,计算单元总是能找到工作不会闲着,这等于是“隐藏”了访问延迟。当然这就需要很多的片上资源来保证计算单元的任务切换开销足够小才行。
总结

在GPGPU概念普及的当下,也有一些抛弃贴图单元用着色器核心做贴图过滤的尝试。方法就是在着色器核心计算贴图像素地址后用通用内存访问指令获取贴图像素,再用着色器做双线性或三线性插值计算。不过专门设计一个贴图模块来高效处理T-OPs级别的计算还是主流。
参考


  • ^abcdefCMU 15-769 VISUAL COMPUTING SYSTEMShttp://graphics.cs.cmu.edu/courses/15769/fall2016/
  • ^各向异性过滤https://zh.wikipedia.org/wiki/%E5%90%84%E5%90%91%E5%BC%82%E6%80%A7%E8%BF%87%E6%BB%A4
  • ^EXT_texture_filter_anisotropichttps://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_filter_anisotropic.txt
  • ^Texture Cacheshttps://fileadmin.cs.lth.se/cs/Personal/Michael_Doggett/pubs/doggett12-tc.pdf
  • ^S3 Texture Compressionhttps://www.khronos.org/opengl/wiki/S3_Texture_Compression
  • ^Ericsson Texture Compressionhttps://en.wikipedia.org/wiki/Ericsson_Texture_Compression
  • ^Adaptive scalable texture compressionhttps://en.wikipedia.org/wiki/Adaptive_scalable_texture_compression
  • ^PVRTC: the most efficient texture compression standard for the mobile graphics worldhttps://www.imaginationtech.com/blog/pvrtc-the-most-efficient-texture-compression-standard-for-the-mobile-graphics-world/

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-1-23 09:55 | 显示全部楼层
以现在的硬件, 把附近四个像素都读出来, 在PixelShader里按自定义办法显式插值,性能上亏的也不多吧
发表于 2022-1-23 09:55 | 显示全部楼层
以mipmap为例,原本是一条sample指令就行了,现在需要8个mem load,alpha、beta各一个ALU,d至少14条ALU,7个lerp至少14条ALU,总共至少38条指令。一个shader也很可能不止一条sample指令。贴图模块的算力绝对不容忽视。
发表于 2022-1-23 10:04 | 显示全部楼层
发表于 2022-1-23 10:07 | 显示全部楼层
不要小看现在的硬件,你提到的算法能用的GPU里面远复杂的多,插值方式也很多,再加上MSAA,sample的问题就在这里,极大的memory 压力,所以又有一大堆考虑如何减轻压力的方法, 压缩,vrs,al based sample。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2025-5-14 16:13 , Processed in 0.144722 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表