3.1管道阶段(pipeline stages)
为了了解管道是如何运作的,我们只需要看看它的名字。数据在管道的一端作为输入提交,然后由第一管道阶段进行处理。该数据由基于矢量的变量组成,最多包含四个分量。在第一阶段的处理完成后,修改后的输出数据被传递到下一阶段。然后将下一组数据带入第一阶段。这意味着前两个阶段同时处理不同的数据。重复该过程,直到整个流水线同时对输入数据的不同部分进行操作。
管道体系结构特别允许不同的管道阶段同时执行多个操作,这允许在单个数据项通过管道时对其执行许多专门的过程。一旦数据项到达管道的末端,它就会被存储到输出资源中,然后主机应用程序可以根据需要使用该输出资源。这种流水线概念是一种简单但强大的处理技术,本质上类似于装配线。图3.1显示了管道如何处理数据。
开发人员的任务是正确配置管道的每个阶段,以在数据从管道末端出现时获得所需的结果。通过操纵管道的每个单独阶段的状态来执行管道配置过程。通过将管道组织成多个阶段,Direct3D11有效地将相关的状态集组合在一起,并整合它们的操作方式。有两种不同类型的管道阶段,即固定功能阶段和可编程着色器阶段。这两种阶段类型都有一些共同的概念,即数据如何流经它们,以及应用程序如何操作它们的状态。以下部分将探讨这两种类型的状态,并提供如何使用它们的一些一般概念。
3.1.1固定功能管道阶段
固定功能流水线阶段对传递给它们的数据执行一组固定的操作。它们执行特定的操作,因此提供了“固定”范围的可用功能。它们可以通过各种方式进行配置,但它们总是对传递给它们的数据执行相同的操作。
这个概念的一个有用的类比是考虑正则函数在C++中是如何工作的。首先向函数传递一个预定义的参数列表。然后,它处理数据并将结果作为输出返回。函数的主体表示固定函数阶段执行的操作,而输入参数是可用配置和实际输入数据。我们不能更改函数体,但我们可以控制在处理输入数据的过程中使用哪些选项。
在前几代Direct3D中,这种类型的固定功能的一些示例包括更改顶点剔除顺序(现在是光栅化器阶段的一部分)、选择深度测试功能(现在是输出合并阶段的一个部分)以及设置Alpha混合模式(也是输出合并阶段)的能力。这种对单一功能领域的关注主要是出于性能考虑。如果为特定任务设计了特定的管道阶段,通常可以对其进行优化,以比通用解决方案更有效地执行该任务。
在Direct3D 9中,通过针对要改变的每个单独设置调用API函数来执行改变这些固定功能阶段的状态。这需要执行许多API调用来为每个阶段配置特定的处理设置。根据场景内容和渲染配置,API调用的数量很容易相加,并开始导致性能问题。Direct3D10引入了状态对象的概念来代替这些单独的状态设置。状态对象用于通过单个API调用配置完整的功能状态。这大大减少了配置状态所需的API调用次数,也减少了运行时所需的错误检查量。Direct3D11遵循这种状态对象范例。应用程序必须用描述结构描述所需的状态,然后从中创建一个状态对象,该对象可用于控制固定功能管道阶段。
一次创建完整状态的要求将状态验证从运行时API调用转移到状态创建方法。如果将不兼容的状态配置在一起,则在创建时会返回一个错误。创建后,状态对象是不可变的,不能修改。因此,无法为固定函数管道设置无效状态,这有效地消除了状态设置API调用的验证负担。总的来说,这种使用状态对象来表示管道状态的系统允许更精简的管道配置,只需要最少的应用程序交互。
3.1.2可编程管道阶段
可编程流水线级包括流水线的剩余级。这里的“可编程”一词意味着这些阶段可以执行用高级着色语言(HLSL)编写的程序。使用与上面相同的C++类比,可编程阶段允许您定义所需的输入参数和函数体。事实上,在这些阶段运行的程序是作为HLSL中的函数编写的。
可编程管道阶段其实就是所谓的要编与HLSL.
执行程序的能力使这些流水线阶段可以用于各种各样的处理任务。这与固定功能阶段形成了对比,固定功能阶段用于非常特定的任务,并且只提供少量的可配置性。在这些可编程管道阶段中执行的程序通常被称为着色器程序,这个名称继承自可编程性的早期步骤,当时像素着色器阶段最初用于修改对象对照明的反应。随着越来越多的可编程阶段被添加到管道中,名称着色器被用来指代所有可编程管道阶段。在整本书中,我们将把这个术语与可编程阶段互换使用。在接下来的部分中,我们将首先从高级别来看这些可编程着色器阶段,然后在建立基本概念后深入了解其架构的细节。
通用着色器核心
所有可编程着色器阶段都建立在一个公共的功能基础上,该基础被称为公共着色器核心。通用着色器核心定义了管道阶段的通用输入和输出设计,提供了所有可编程着色器阶段都支持的一组内在功能,以及可编程着色器可以使用的资源接口。
图3.2提供了通用着色器核心如何操作的可视化表示。如上所述,数据流入场景的顶部,在着色器核心内进行处理,然后作为场景底部的输出流出。在这些阶段中执行的着色器程序是用HLSL编写的用于特定目的的函数。在着色器阶段内处理数据时,着色器程序可以访问应用程序绑定到该阶段的常量缓冲区、采样器和着色器资源视图。一旦着色器程序完成了对其数据的处理,该数据就会从后台传递出去,并引入下一段数据以重新开始该过程。
可编程流水线阶段的可配置性不限于在着色器程序内执行的处理。这些阶段的输入和输出数据结构也由着色器程序指定,该程序提供了从一个阶段到另一个阶段传递数据的灵活方式。一些规则被强加在阶段之间的这些接口上,例如要求特定阶段的接口中包括某些类型的数据。所需输出的典型示例是光栅化器之前的一个级提供可用于确定渲染目标的哪些像素被基元覆盖的位置输出。根据传递数据的阶段,参数要么未经修改地提供,要么可以在传递到下一阶段之前进行插值。
虽然所有可编程阶段共享一组共同的功能,但每个阶段也可以提供其独有的额外专业特性和功能。这些特性和功能通常与输入和输出语义有关,因此仅适用于特定阶段。在本章后面描述管道的每个阶段时,将更详细地讨论每个阶段的单独行为。
有了如此多的灵活性和各种不同的管道阶段,有许多算法不需要所有阶段都是活动的。因此,可以通过清除其着色器程序来禁用可编程着色器阶段。此外,由于在每个阶段中进行的处理具有很大的灵活性,因此很有可能在所有可编程阶段的不同组合中执行一些或全部所需的工作。这种选择在哪里执行特定计算的能力对我们有利。如果可以在任何数据放大之前进行计算,则可以用更少的运算来计算相同的数据。我们将在第8章“网格渲染”中看到一个很好的例子,其中在对模型进行曲面之前执行顶点蒙皮,以减少蒙皮的顶点数量。(关于顶点蒙皮的详细信息,请参阅第8章。)
着色器核心架构
在上一节中,我们已经了解了可编程阶段如何操作的一般概念。它接收来自前一阶段的输入,在其上执行HLSL程序,并将结果传递到下一个管道阶段。然而,这只是对实际情况的概述。在可编程阶段执行的着色器程序实际上是从HLSL编译成基于矢量寄存器的汇编语言,设计用于GPU中的专用着色器处理器核心。尽管所有着色器程序都必须用HLSL编写,但在渲染管道中使用之前,它们仍必须编译为此汇编字节代码。
我们可以从这种汇编语言中学到很多信息。它定义了一组特定的寄存器,编译器可以使用这些寄存器将HLSL程序映射到汇编语言。寄存器通常是四分量矢量寄存器,也可以使用单独的分量来提供标量寄存器功能。着色器核心有用于接收其输入数据的寄存器、用于执行计算的临时寄存器、用于与资源交互的寄存器以及用于将数据传出后台的寄存器。汇编语言指令使用这些寄存器来执行它们各自的操作。通过了解汇编程序如何使用这些寄存器,我们可以深入了解着色器阶段的操作方式。
首先,我们将考虑图3.2中所示的常见着色器核心概述,但这次我们将从汇编语言的角度进行查看。图3.3显示了通用着色器核心的组装版本。
如图3.3所示,着色器核心的输入在v#寄存器中提供。由于它们为介段提供输入,因此它们自然是只读的。执行着色器程序时,其输入数据在v#寄存器中可用。读取数据后,可以对其进行操作并将其与其他数据组合,并且任何中间计算都可以存储在r#和x#[n]寄存器中。这些被称为临时寄存器,由于它们包含中间值,因此着色器程序可以读取和写入它们。纹理寄存器(t#)、常量缓冲寄存器(cb#[n])、立即常量缓冲寄存器[icb[index])和无序访问寄存器(u#)也可用作数据源。这些寄存器用于提供对设备内存资源的访问,如第2章所述,并且除了无序访问寄存器之外,它们都是只读的。最后,将传递到下一个流水线阶段的计算值写入输出寄存器(o#)。当着色器程序终止时,存储在输出寄存器中的值将传递到下一阶段的输入寄存器,在那里重复该过程。其他一些特殊用途登记册只在某些阶段提供,因此我们将把对它们的讨论推迟到本章稍后部分。
通常,开发人员不需要检查已编译着色器程序的程序集列表,除非存在性能问题。这使得了解程序集指令如何操作的细节变得不那么重要。即便如此,掌握基于装配的世界的基本知识仍然很有帮助。例如,当开发人员为着色器程序定义输入和输出数据结构时,他们必须知道每个阶段可以使用多少输入和输出矢量的限制。这是由该特定阶段可用的输入和输出寄存器的数量决定的。类似地,常量缓冲区、纹理和无序访问资源的可用数量也受到它们各自寄存器的限制。这是非常重要的信息,在我们进行每个管道阶段的讨论时都应予以考虑。
GPU架构
GPUl架构的不同,相同架构版本的不同导致了着色器程序兼容性,效能的差异。
即使有严格定义的汇编语言规范,也不需要实际的GPU硬件来直接实现该规范。有许多不同的体系结构实现,不同供应商的实现可能差异很大。事实上,即使是来自同一供应商的连续几代GPU硬件也可能存在显著差异。这使得预测给定着色器程序在当前或未来GPU上执行的效率变得异常困难。根据执行程序的GPU的架构,一种特定的存储器访问模式可能比另一种更高效,但对于不同的架构,情况可能恰恰相反。
由于实现之间存在巨大的差异,因此在此处提供详细信息是不切实际的。我们邀请读者更详细地探讨这个引人入胜的话题,在(Fathalian)中可以找到一个很好的起点。然而,我们仍然可以提供GPU如何组织的总体概念,以便在本书稍后的讨论中有一个可供借鉴的背景。GPU已经发展成为一个大规模并行处理器,容纳了数百个单独的ALU处理核心。这些处理器可以运行自定义程序,并且可以访问大量内存,从而实现高带宽数据传输。每个GPU使用某种形式的存储器高速缓存系统来减少存储器请求的有效延迟,尽管高速缓存系统是特定于供应商的并且其低级别设计通常未被公开。在过去,为了获得最大的可用性能水平,有必要了解特定目标GPU的各个体系结构细节。这一趋势可能会持续一段时间,因为GPU仍在以非常快的速度发展