上帝视角看GPU(5):图形流水线里的不可编程单元

2024-03-05 01:36

本文主要是介绍上帝视角看GPU(5):图形流水线里的不可编程单元,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


  • 【GPU】图形流水线基础
  • 【GPU】逻辑上的模块划分
  • 【GPU】部署到硬件
  • 【GPU】完整的软件栈


前几期我们过了一遍GPU的软硬栈。这次我们将深入GPU图形流水线的一些细节,看看那些不可编程的模块是怎么工作的。

对于GPU的图形流水线来说,最核心最重要的一个组件就是光栅化器。

它的存在,直接决定了GPU在实时渲染方面的优势。以至于很多时候光栅化就是GPU图形流水线的代称。

以前说过,经过vertex shader之后,每个顶点上都有了转换后的属性。

包括位置、法线方向、颜色、纹理坐标等。

经过primitive assembler之后来到光栅化阶段,转换成像素。

所以,光栅化这个操作,本质上就是把三个顶点上的信息,

插值到这个三角形覆盖的每个像素上,交给pixel shader。

Primitive assembler和光栅化总是连在一起, 因此成为了广义的光栅化器。

下面我也会把这两部分合并起来描述算法。要完成这样的插值,常见的方法称为扫描线算法。

顶点上有一系列的属性,根据三个顶点的位置和属性变化的程度,可以算出这个三角形覆盖的区域里。

从一个像素挪到右边或者下边,各个属性会改变多少。这称为ddx和ddy。

对于一个三角形来说,属性的ddx和ddy都是常量,只要算一次就行。

接着,从最靠上的顶点开始,沿着轮廓一行一行往下扫。

每一行根据三角形轮廓就能知道应该从哪里开始,到哪里结束。

往右一个像素,属性增加一次ddx,往下一行,属性增加一次ddy。

这样扫描生成三角形覆盖的所有像素。


那么,像素在什么情况下认为被三角形覆盖?这叫做光栅化规则。普通模式下,光栅化三角形看的是像素中心是否在三角形之内。

光栅化线看的是线是否经过像素里的一个菱形区域,另一种模式是只要沾到一点就算覆盖。这叫保守式光栅化,常用于体素化的需求。

比如前几年很热门的SVO cone tracing, 就用保守式光栅化来把整个场景变成体素的表达。

光栅化的算法有了,直接放到硬件上,就成了立即式光栅化。

这个做法很淳朴,用ASIC把刚才描述的算法变成硬连线。

在算法完全固定的情况下,ASIC的效率远高于FPGA和可编程单元。, 立即式光栅化生成的像素,经过流水线后面的几个阶段,写入内存里的渲染目标。

渲染目标可能是纹理也可能是帧缓存。

对于大三角形来说,这么做性能非常高。因为只要算一次ddx、ddy,后面一路累加过去就行,可以不被打断地一直执行同样的操作。

但是,如果三角形层层叠叠,就得反复写入内存,带宽占用很大,功耗高。

对PC渲染的需求来说,只要性能高,功耗高一点也可以接受的,

所以往往会选择立即式光栅化的方案。

这张图是我在AMD的GPU上得到的,其中的颜色表达了像素到达pixel shader的顺序。

从这里可以看出,这个GPU采取了32个像素的高度,一条一条的光栅化方式。

而CPU光栅化的WARP,就是一个一个像素扫过去的方式。

到了移动平台,这就够呛了。移动平台更看重的是性能功耗比。

如果性能只有一半,但功耗只有四分之一,也会考虑采纳。

于是,移动平台上,光栅化往往采用tile-based方案。它把渲染目标划分成很多固定大小的tile,常见的是32x32。

每个tile包含一个列表,存有和这个tile相交的所有三角形。所以tile-based光栅化不再是一个一个三角形处理,而是一批一批处理。

这样的GPU,需要有一个片上内存充当cache的角色,不需要大,但访问速度远远高于内存。

对于每个tile,先会把渲染目标的对应区域载入GPU的片上内存。接着用扫描线算法,把列表里的三角形都渲染上去。,最后把片上内存里的结果存到渲染目标。

然后开始处理第二个tile。

不管三角形如何层层叠叠,每个tile每次对内存的读写,总是只有32x32个像素,远低于立即式。

但因为一个三角形没法一直填充下去,会因为tile而被打断,性能其实是降低的。

只是相比之下功耗降得更多。

这是在高通的GPU上跑出来的光栅化顺序图。

可以看到每个tile大小固定,一个一个tile铺成了整个屏幕。

为了让大家加深印象,这里举两个实际的场景,比较一下立即式和tile-based在工作流程上的区别

同样是渲染两个三角形。

第一种情况,把它们渲染到同一张纹理。

第二种情况,把它们分别渲染到两张纹理。

对于立即式来说,都是把三角形光栅化出去。

两个三角形是不是到同一个纹理无所谓。操作是一样的。因此两者的性能和功耗区别可以忽略不计。

对于tile-based,这俩就很不一样了。

第一种情况,光栅化的流程是,第一个tile载入到片上内存,渲染两个三角形,存到纹理;

第二个tile载入到片上内存,渲染两个三角形,存到纹理;

依此处理完所有tile。

第二种情况的流程就变成,纹理A的第一个tile载入到片上内存,渲染三角形A,存到纹理A;

纹理A的第二个tile载入到片上内存,渲染三角形A,存到纹理A;

依此处理完纹理A的所有tile;

纹理B的第一个tile载入到片上内存,渲染三角形B,存到纹理B;

纹理B的第二个tile载入到片上内存,渲染三角形B,存到纹理B;依此处理完纹理B的所有tile。

注意,在片上内存里的操作非常快,访问内存里的纹理,慢得多而且耗电得多。

因此第一种情况比第二种好情况得多。这也是为什么在tile-based GPU上,切换渲染目标成为大忌。

这几年API的改进集中于提供subpass和invalidate等操作,可以让开发者决定是否把纹理的tile载入片上内存,以及渲染后是否存到纹理,以减少这部分的开销和功耗。

继续往后看。光栅化产生的像素,会进入pixel shader,然后是output merger。

场景是存在遮挡的,近的会挡住远的。

之前说过,这是在output merger里通过深度测试来完成。

假设光栅化产生了像素A,跑完后面的流程,写入渲染目标,又在同一位置产生像素B。

这里会产生两个问题。

第一,如果像素B比像素A还近,,那它跑完后面的流程之后,会覆盖掉像素A。

这使得像素A经过的pixel shader和output merger完全浪费了。

第二,如果像素B比像素A还远,也得等到运行了pixel shader,进入output merger,才能发现有遮挡,才抛弃掉像素B。

那么像素B的pixel shader也白运行了。能不能把深度测试从output merger挪到pixel shader之前呢?

不总是可以的,因为pixel shader不但能输出颜色,也能输出深度。

如果光栅化产生的深度和pixel shader输出的深度顺序不一致,在pixel shader之前执行深度测试就会出错。

为解决第二个问题,GPU引入的功能称为early-z。

在渲染状态符合条件的情况下,驱动会检查一下pixel shader,如果不输出深度、不用discard丢弃像素,就启用early-z。

像素在进入pixel shader之前提前进行一次深度测试。

如果已经被挡住,就不往下走,直接丢弃掉。


在tile-based的光栅化上,有的GPU会有个称为TBDR的模式,tile-based deferred rendering。

TBDR在开启深度测试的情况下,把光栅化插值得到的像素属性都写入片上内存。

这时候不可见的像素就被抛弃了,只有可见的继续往下走,同时解决那两个问题。

但是它无法独立存在的,也是要一系列条件都符合的情况下才能启用,否则退回到tile-based模式。

既然立即式性能高,tile-based性能功耗比高,能不能取长补短一下?


有的,Maxwell之后的NVIDIA GPU,就采用了两者的结合,称为tiled caching。

它的tile巨大,256x256这个级别,cache也很大,不光像素,还可以把tile需要的几何也载入cache。

这么做降低了内存访问,性能和性能功耗比都更高了。这里仍然可以用一张光栅化的顺序图来看到这个情况。

前面说的,都是用硬件直接构造光栅化。它的性能优势来自于ASIC上执行固定的算法,但因此牺牲了灵活性。

有没有可能用软件来构造光栅化呢?这里说的软件,不是指在CPU运行,而是在GPU的可编程单元里运行。

有些需求使得软件光栅化变得很重要。

第一个需求,小三角形的光栅化性能。

在实际中,硬件光栅化的输出并不是一个一个像素,而是一个一个2x2的像素块。

这称为一个quad,quad之内每个像素都有邻居,于是可以在pixel shader里获得任何变量的ddx和ddy,只要和邻居一减就出来了。


这四个像素如果有在三角形之外的,之后才会被丢弃。

对于大三角形来说,最多也就是边缘的像素存在浪费。

但对于小于一个像素的三角形,这就浪费了3/4。

因此,对于大量三角形都小于一个像素的时候,构造一个以像素为单位的软件光栅化器,可以避免浪费,性能反而更高。

在UE5的Nanite里就是这么做的优化。另一个需求,在无法使用硬件光栅化的时候执行光栅化。

典型的是Intel在08年的Larrabee。上面没有常规的流式处理器,而是堆了48个奔腾的x86 CPU,加上很宽的SIMD指令集。

在那些CPU上执行的是个特制的软件光栅化算法,自适应细分的tile-based光栅化。

首先,像普通的tile-based一样,, 把整个渲染目标分成一系列tile,但是每个tile较大,至少64x64。

每次都是进行宽度为16的SIMD计算。这部分细节,






有兴趣的朋友可以看Salvia渲染器,里面有这个算法的开源实现。

Salvia渲染器: https://github.com/wuye9036/SalviaRenderer/

同样也是每个tile包含与它相交的三角形列表。接着把这个tile等分成16个小tile,测试每个小tile和三角形的相交情况。

完全覆盖了,就全部填充。完全不相交,就跳过。

如果部分覆盖,就把小tile再继续分成16个更小的tile,再次测试,以此类推直到像素级别为止。


后来,Larrabee项目停止,砍掉显示输出能力后改造成Xeon Phi运算卡,那个SIMD指令集成了AVX-512。

普遍猜测是功耗控制不住。NVIDIA也有个类似的工作,用CUDA构造软件光栅化。既然光栅化器也可以用软件构造了,那么其他固定流水线模块呢?

挨个过一遍吧。Input assembler负责读取和组装顶点。

在shader能直接访问buffer之后,, 这很容易就能改成让shader读取。

有时候性能还能更高。比如位置和法线用不同的index的情况,

如果必须用硬件input assembler,就得重新组织buffer,有些数据得复制组合多份。

在shader里写代码读取的话,可以支持多个index buffer,总的数据量更小。这在虚幻引擎里面叫做manual vertex fetch模式,用得越来越多。

Stream output原理也一样,无非就是改成写入UAV。Tessellator是在三角形内生成新的顶点, 连起来变成一组三角形。

我发明过一个方法,通过查找表迅速定位顶点之间的连接关系,在compute shader里实现tessellator。只要修改查找表就能更换连接的pattern,不一定要按照硬件写死的。

这个算法被《文明》系列游戏借鉴了,用于地形的细分,通过可定制pattern的思路,做到比硬件tessellator更高的速度。

最后的output merger,执行的是这个流程图,变成代码是非常直接了当的事情。

唯一需要注意的是alpha blending如果没有硬件帮助,性能会受一些影响。

如此看来,图形流水线里的几乎每一部份,都可以用可编程单元来构造。唯一必须用硬件才有绝对性能优势的是纹理采样。

它需要对纹理进行读取、解压、插值,计算量不小,算法很固定,适合硬件连线去做。

按照Larrabee的研究,用软件实现性能会降低12到40倍。

而对于那些所谓“通用GPU,只有计算流水线没有图形流水线的诈骗货,其实也可以用这样的软件方式构造图形流水线,在驱动里无缝衔接,成为真正的GPU。

我们把光栅化的各种方法过了一遍,并提出了如何用可编程的单元构造整条图形流水线。下一期,我们将关注于另一条越来越重要的流水线,光线跟踪。

欢迎继续收看,再见。

来自:龚大的杂货铺的教学视频

这篇关于上帝视角看GPU(5):图形流水线里的不可编程单元的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/774907

相关文章

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

AI Toolkit + H100 GPU,一小时内微调最新热门文生图模型 FLUX

上个月,FLUX 席卷了互联网,这并非没有原因。他们声称优于 DALLE 3、Ideogram 和 Stable Diffusion 3 等模型,而这一点已被证明是有依据的。随着越来越多的流行图像生成工具(如 Stable Diffusion Web UI Forge 和 ComyUI)开始支持这些模型,FLUX 在 Stable Diffusion 领域的扩展将会持续下去。 自 FLU

如何用GPU算力卡P100玩黑神话悟空?

精力有限,只记录关键信息,希望未来能够有助于其他人。 文章目录 综述背景评估游戏性能需求显卡需求CPU和内存系统需求主机需求显式需求 实操硬件安装安装操作系统Win11安装驱动修改注册表选择程序使用什么GPU 安装黑神话悟空其他 综述 用P100 + PCIe Gen3.0 + Dell720服务器(32C64G),运行黑神话悟空画质中等流畅运行。 背景 假设有一张P100-

如何使用Ansible实现CI/CD流水线的自动化

如何使用Ansible实现CI/CD流水线的自动化 持续集成(CI)和持续交付(CD)是现代软件开发过程中的核心实践,它们帮助团队更快地交付高质量的软件。Ansible,作为一个强大的自动化工具,可以在CI/CD流水线中发挥关键作用。本文将详细介绍如何使用Ansible实现CI/CD流水线的自动化,包括设计流水线的结构、配置管理、自动化测试、部署、以及集成Ansible与CI/CD工具(如Jen

Unity3D自带Mouse Look鼠标视角代码解析。

Unity3D自带Mouse Look鼠标视角代码解析。 代码块 代码块语法遵循标准markdown代码,例如: using UnityEngine;using System.Collections;/// MouseLook rotates the transform based on the mouse delta./// Minimum and Maximum values can

Go并发模型:流水线模型

Go作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,Go并发模型有多种模式,通过流水线模型系列文章,你会更好的使用Go的并发特性,提高的程序性能。 这篇文章主要介绍流水线模型的流水线概念,后面文章介绍流水线模型的FAN-IN和FAN-OUT,最后介绍下如何合理的关闭流水线的协程。 Golang的并发核心思路 Golang并发核心思路是关注数据流动。数据流动的过程交给cha

GPU 计算 CMPS224 2021 学习笔记 02

并行类型 (1)任务并行 (2)数据并行 CPU & GPU CPU和GPU拥有相互独立的内存空间,需要在两者之间相互传输数据。 (1)分配GPU内存 (2)将CPU上的数据复制到GPU上 (3)在GPU上对数据进行计算操作 (4)将计算结果从GPU复制到CPU上 (5)释放GPU内存 CUDA内存管理API (1)分配内存 cudaErro

第六章习题11.输出以下图形

🌏个人博客:尹蓝锐的博客 希望文章能够给到初学的你一些启发~ 如果觉得文章对你有帮助的话,点赞 + 关注+ 收藏支持一下笔者吧~ 1、题目要求: 输出以下图形

PyInstaller问题解决 onnxruntime-gpu 使用GPU和CUDA加速模型推理

前言 在模型推理时,需要使用GPU加速,相关的CUDA和CUDNN安装好后,通过onnxruntime-gpu实现。 直接运行python程序是正常使用GPU的,如果使用PyInstaller将.py文件打包为.exe,发现只能使用CPU推理了。 本文分析这个问题和提供解决方案,供大家参考。 问题分析——找不到ONNX Runtime GPU 动态库 首先直接运行python程序

一个图形引擎的画面风格是由那些因素(技术)决定的?

可能很多人第一直覺會認為shader決定了視覺風格,但我認為可以從多個方面去考慮。 1. 幾何模型 一個畫面由多個成分組成,最基本的應該是其結構,在圖形學中通常稱為幾何模型。 一些引擎,如Quake/UE,有比較強的Brush建模功能(或應稱作CSG),製作建築比較方便。而CE則有較強的大型地表、植被、水體等功能,做室外自然環境十分出色。而另一些遊戲類型專用的引擎,例