GPU精粹2——高性能图形芯片和通用计算编程技巧 流式编程

33.3  基于GPU的数据结构
前面几节以描述了GPU及其编程模型,现在开始深入研究在当前GPU上真实数据结构的细节。33.1节和33.2节的抽象继续适用于这里的数据结构,但是当前GPU的体系结构限制使真实的实现稍微更复杂了些。
首先描述基本结构的实现:多维数组和结构体。然后在第33.3.3和33.3.4以节中转移到更高级的结构:静态和动态的稀疏结构。
33.3.1  多维数组
如果GPU已经提供了1D、2D和3D纹理,为什么还需要介绍多维数组?这里有两个理由:首先,目前的GPU只提供2D光栅化和2D帧缓冲区,这意味着可以容易更新的惟一一种类型的纹理是2D纹理,因为它是2D帧缓冲区的一个天然替代品;其次,纹理现在在每一维的大小方面有限制,例如,目前的GPU不支持超过4 096个元素的1D纹理。如果GPU可以写入一个N-D的帧缓冲区(这里N = 1, 2, 3, …)而且没有大小限制,本节将会不太必要。
有了这些限制,带有最近相邻过滤的2D纹理是几乎所有基于GPGPU的数据结构建立的基础。注意直到最近,每个纹理维度的大小都要求是2的幂,但是这个限制终于不再出现。
下面所有的例子都使用地址变换把一个N-D数组地址转换成一个2D纹理地址。虽然最小化这个地址变换的开销是很重要的,但基于CPU的多维数组也必须执行地址变换才能查到内在的1D数组的值。事实上,GPU的纹理寻址硬件可以帮我们非常高效地进行这些变换。然而,目前的GPU确实遇到了有关这些地址变换的一个问题:浮点地址的限制。本书的第32章讨论了有关使用浮点而不是整型地址时,介绍了重要限制和错误,那些问题适用于在本节中呈现的技术。
1. 一维数组
一维数组的表示是把数据打包入一个2D纹理,如图33-5所示。当前的GPU因此可以表示最多包含16 777 216(4,096 × 4,096)个元素的一维数组。每次从一个片段或顶点程序中访问这个打包的数组时,1D地址必须被转换成一个2D纹理座标。
图33-5  一维数组打包入二维数组
图注:这么做是为了避免当前GPU上存在的1D纹理大小限制。
这个地址变换的两个版本在程序清单33-1和33-2中以Cg语法表示。程序清单33-1(Buck等,2004)的代码使用了矩形纹理,它使用整型地址,而且不支持重复贴片的地址模式。注意:CONV_CONST是基于纹理大小的常量,应该是预先计算的而不是为每个流元素重新计算。33.4节描述了一项技术,用于计算类似于CONV_CONST这样的值,在Cg编译器中这个特性叫做程序特化(program specialization)。通过这个优化,程序清单33-1可以编译成3条汇编指令。
程序清单33-1  用于整型寻址纹理的1D到2D地址变换
float2 addrTranslation_1DtoRECT( float address1D, float2 texSize )
{
  // 参数解释:
  // - address1D是一个索引入大小为N的一维数组的地址
  // - texSize是存储了这个一维数组的矩形纹理大小

  // 第1步) 把1D地址从[0,N]变换到[0,texSize.y]
  float CONV_CONST = 1.0 / texSize.x;
  float normAddr1D = address1D * CONV_CONST;

  // 第2步) 把[0,texSize.y]的1D地址转换到2D。
  // 首先执行第1步允许我们用frac和floor
  // 高效地计算2D地址,而不用取模和除法。
  // 注意y分量的floor是由纹理系统执行的。
  float2 address2D = float2( frac(normAddr1D) * texSize.x, normAddr1D );
  return address2D;
}
程序清单33-2演示了一个地址变换程例,可以用于传统的2D纹理,即使用规范化的[0, 1]地址。如果CONV_CONST是预先计算的,那么这个地址变换只占用两条片段汇编指令。它通过使用重复贴片的寻址模式(如GL_REPEAT),也有可能从程序清单33-2中除去frac指令实现。这就把这个变换减少为一条汇编指令。然而,这个优化可能在一些硬件和纹理配置上有些麻烦,所以它应该小心地在目标体系结构上测试。
二维数组
二维数组只是表示为2D纹理。它们的最大大小受限于GPU驱动。当前GPU的限制将2048 × 2048像素~4096 × 4096像素,取决于显示驱动和GPU上。这些限制可以通过图形API查询到。
程序清单33-2  用于规范化地址纹理的1D到2D地址变换
float2 addrTranslation_1Dto2D( float address1D, float2 texSize )
{
  // 参数解释:
  // - address1D是一个索引入大小为N的一维数组的地址
  // - texSize是存储了这个一维数组的2D纹理大小

  // 注意:在运行核之前预计算CONV_CONST。
  float2 CONV_CONST = float2( 1.0 / texSize.x,
                                    1.0 / (texSize.x * texSize.y );

  // 返回一个规范的2D地址(值在[0,1]之内)
  float2 normAddr2D = address1D * CONV_CONST;
  float2 address2D = float2( frac(normAddr2D.x), normAddr2D.y );
  return address2D;
)
3. 三维数组
三维数组可以以3种方式存储:3D纹理中,每一片存在一个单独的2D纹理中,或打包进单个2D纹理中(Harris等2003,Lefohn等2003,Goodnight等2003)。这些技术中的每一项都有优点和缺点,会影响在应用程序中的最佳表现。
最简单的方式是使用一个3D纹理,有两个明显的好处。第一是访问存储器时不需要地址变换计算。第二个好处是GPU的内建三线过滤可以用来容易地建立数据的高质量体渲染。缺点是在每个渲染遍中GPU最多只能更新4个片——这就需要更多的遍才能写入整个数组。
第二个解决方案是把3D纹理的每个片存储在一个单独的2D纹理中,如图33-6所示。优点是,就像内建的3D表示,数据访问时不需要地址变换,而且每个片都可以简单地更新,不需要渲染到3D纹理片的API支持。缺点是体不能再真正地随机访问,因为每个分片是一个独立的纹理。在核运行之前,程序员必须知道哪个片将被访问,因为片段和顶点程序在运行时不能动态地计算该访问哪个纹理。
图33-6  把3D纹理存储在分开的2D片中
图注:三维纹理可以把每个片存在独立的2D纹理中。优点是访问数据时不需要地址变换,以及2D片可以容易地在独立的渲染遍中更新。缺点是每个片必须在独立的渲染遍中更新,以及要访问的片号必须在核访问数据前就知道。
第三个方法是把三维数组打包进一个2D纹理中,如图33-7所示。这个解决方案对每次数据访问都需要一个地址变换,但是整个3D数组可以在一个渲染遍中更新,不需要渲染到3D纹理分片的功能。在一个渲染遍中更新整个体的能力可能会有很大的性能优势,因为当处理大的流时,GPU的并行性可以更好地发挥。另外地,不像2D片的布局,整个3D数组可以在核中随即地访问。
第二和第三个框架的缺点是GPU的内建三线过滤不能用于高性质的体数据渲染。幸运的是,备用的体渲染算法可以从这些复杂的3D纹理格式高效地渲染高性质、过滤的图像(参见本书的第41章)。
图33-7  3D数组展平到单个2D纹理
图注:三维数组可以展平到单个2D纹理(或者pbuffer),所以整个体都可以在一个渲染遍中更新,而且整个三维数组都可以随机访问。数据可能按片来展开(如同这里显示的),或以线性打包,如同程序清单33-3所描述的。
程序清单33-3中的Cg代码演示了一种地址变换形式,把3D地址转换成打包表示法的2D地址。这个打包和在程序清单33-1和33-2中使用的一维数组相同。它只是在把1D空间打包进2D纹理之前,把3D地址变换到一个大的1D空间。这个打包框架在Buck等2004中呈现。注意,这个框架可以使用程序清单33-1或33-2演示的转换,取决于数据纹理的类型(2D或矩形)。
程序清单33-3  把3D地址变换到2D地址
float2 addrTranslation_3Dto2D(float3 address3D,
                                    float3 sizeTex3D,
                                    float2 sizeTex2D)
{
  // 参数解释:
  // - address3D是一个索引入大小为sizeTex2D的三维数组的地址
  // - sizeTex2D是存储了这个三维数组的2D纹理大小

  // 第1步} 纹理大小常量(这个应该预计算!)
  float3 SIZE_CONST = float3(1.0, sizeTex3D.x,
                                  sizeTex3D.y * sizeTex3D.x);

  // 第2步} 把3D地址变换到[0, sizeTex2D.y]的1D地址
  float address1D = dot( address3D, SIZE_CONST );

  // 第3步} 把[0, texSize.y]的1D地址变换成2D,使用程序清单33-1
  // 定义的1D到2D变换函数。
  return addrTranslation_1Dto2D( address1D, sizeTex2D );
}
用于基于片的,备选的打包框架的Cg代码如程序清单33-4所示。该框架把3D纹理的片打包进2D纹理中。这个系统的一个难点是2D纹理的宽度必须被三维数组片的宽度整除。好处是内建的双线过滤可以在每个片中使用。注意,如果sliceAddr计算被存储在一个1D查找表纹理中,由address3D.z索引,而且nSlicesPerRow是预计算的,那么整个地址转换可以减少为两条指令(一个1D纹理查询和一个乘增加)。
程序清单33-4  用于把3D纹理的片打包进2D纹理的源代码
float2 addrTranslation_slice_3Dto2D( float3 address3D,
                                             float3 sizeTex3D,
                                             float2 sizeTex2D)
{
  // 注意:这个应该预计算
  float nSlicesPerRow = sizeTex2D.x / sizeTex3D.x;

  // 在片的地址空间中计算一个片的(x,y)
  float2 sliceAddr = float2( fmod(address3D.z, nSlicesPerRow),
                                  floor(address3D.z / nSlicesPerRow) );

  // 把sliceSpace地址转换成2D纹理地址
  float2 sliceSize = float2(address3D.x, address3D.y );
  float2 offset = sliceSize * sliceAddr;

  return addr3D.xy + offset;
}
注意,如果GPU支持3D帧缓冲区的3D光栅化或把纹理从2D映射到3D的能力,就没有理由把三维数组存成2D纹理了。在后一种情况中,GPU会光栅化2D、展平形式的数组,但允许程序员使用3D地址读取它。
4. 多维数组
多维数组可以打包进2D纹理,使用程序清单33-3中演示的打包框架的泛化形式(Buck等,2004)。
33.3.2  结构体
在程序清单33-5中所示的,“结构体的流”定义必须改为“流的结构体”。程序清单33-6中所示。在这个构造中,每个结构体成员都建立了一个独立的流。除此之外,结构体不能包含多于GPU的每个片段可以输出的数据。这些限制是由于片段程序不能指定它们的结果要写入的帧缓冲区地址(也就是,它们不能够执行一个散列操作)。通过把结构体指定为“流的结构体”,每个结构体成员都有相同的流索引,而因此所有成员能被一个片段程序更新。
程序清单33-5  结构体的流
// 警告:像下面例子中演示的“结构体的流”在当前的GPU上不能简单地更新。
struct Foo {
  float4 a;
  float4 b;
};

// 这个“结构体的流”很有问题
Foo foo[N];
程序清单33-6  流的结构体
// 这个“流的结构体”在当前的GPU上可以简单的更新,
// 如果每个结构体中的数据成员数目 <= GPU支持的片段输出数目。
struct Foo {
  float4 a[N];
  float4 b[N];
};

// 为每个成员定义一个独立的流
float4 Foo_a[N];
float4 Foo_b[N];
33.3.3  稀疏数据结构
到现在为止,我们讨论的数组和结构体都是密集(dense)的结构。换句话说,在数组的存储空间中所有的元素都包含有效的数据。然而,有许多问题的高效解需要使用稀疏的数据结构(如列表、树或稀疏矩阵)。稀疏的数据结构是很多优化的基于CPU的算法中的一个重要部分;如果原始的基于GPU的实现使用密集数据结构,则经常会比经过优化的CPU版本慢。除此之外,稀疏的数据结构可以减少算法的存储器需求——在GPU存储器数量有限的情况下的一个重要考虑。
尽管它们很重要,但稀疏数据结构的GPU实现是棘手的。一是更新稀疏的结构通常需要写入一个计算出的存储器地址(即散列)。第二个困难是遍历一个稀疏的结构通常需要一个数量不定的指针引用才能访问数据。这一点棘手的原因是(33.1节),目前的片段处理器是SIMD机器,必须用完全相同的指令处理大批流元素。然而,研究员已经指出一些稀疏的结构可以在目前的GPU上实现。
1. 静态稀疏结构
从描述基于GPU的静态稀疏数据结构开始,它们的结构不在GPU计算期间改变。这样的数据结构包括Purcell的光线加速栅格的三角形的列表(Purcell等,2002),以及Bolz等(2003)、Krüger和Westermann (2003)的稀疏矩阵。在这些结构中,位置和稀疏元素的数目在GPU计算的过程中是固定的。例如,在光线追踪的场景中,三角形的位置和数目并不改变。因为结构是静态的,所以它们不一定要写入计算出的存储器地址。
所有这些结构都使用一个或多个间接层来表现存储器中的稀疏结构。例如,Purcell的光线加速结构由一个三角形列表指针的3D栅格开始,如图33-8所示。3D栅格纹理包含指向三角形列表起点的指针(存储在第二个纹理中)。在三角形列表中的每个入口,依次包含了一个指针,指向存储在第三个纹理中的顶点数据。类似地,稀疏矩阵的结构使用了一个固定次数的间接层来寻找非零矩阵元素。
图33-8  Purcell的稀疏光线跟踪数据结构
图注:这些结构用两种不同的方法解决不规则访问模式的问题。第一种方法是把结构分解成块,在块中的所有元素有相同的访问模式,因此可以一起处理。第二种方法是让每个流元素在每个渲染遍处理列表中的一个项。要处理较多项的元素将会继续计算结果,而已经到达列表结尾的那些将会被关闭。
光线遍历过一个3D栅格,访问三角形列表3D纹理来获取在那个单元的三角形列表的起始位置。三角形列表入口索引入第三个纹理,那里存储了场景中所有三角形的顶点位置和纹理坐标。
分块策略的一个例子可以在Bolz 2003中找到。他们把稀疏矩阵分解成非零元素数量相同的块,并填补上相似的行,以便它们有同样的布局。遍历三角形列表的算法可以在Purcell 2002中找到,它通过条件运行来进行非均匀遍历。所有活动的光线(流元素)在每个渲染遍中处理来自它们对应列表中的一个元素。当它们到达它们的三角形列表结尾的时候,光线变为不活动的,而要处理更多三角形的光线继续运行。
注意,所有流元素都必须用相同访问模式的约束是目前GPU的SIMD运行模型的一个限制。如果未来的GPU支持MIMD流处理,那么访问不规则的稀疏数据结构可能变成非常容易。例如,像素着色器 3.0的GPU提供的有限分支已经比早先几代的GPU提供了更多用于数据结构遍历的选择。
2. 动态稀疏结构
在GPU计算期间更新的基于GPU的稀疏数据结构是研究的活跃领域。两个值得注意的例子是Purcell等2003中的光子图和Lefohn(2003、2004)中的可形变隐表面的表示。本节提供了这些数据结构的简要概览和用于更新它们的技术。
光子图是一个稀疏的、自适应的数据结构,用于估算场景中的辐照度(即到达一个表面的光)。Purcell等(2003)描述了一个完全基于GPU的光子图渲染器。为了要在GPU上建立光子图,他们发明了两种框架,用于在目前的GPU上把数据写入计算出的存储器地址(即散列)。第一项技术计算存储器地址和要存储到相应地址的数据。然后它通过在这些缓冲区上执行一个数据并行的排序操作来执行这个散列。第二项技术是模板路由,使用顶点处理器在计算出的存储器地址所定义的位置绘制大的点。它解决了冲突(当两个数据元素写入了相同的位置),通过一个创造性模板缓冲区技巧——当数据值被画在相同的片段位置时,把它们路由到同一个桶里(像素位置)。非均匀的数据访问方式类似于在前一小节描述的三角形列表的遍历。
另一个基于GPU的动态稀疏数据结构是Lefohn等(2003、2004)用来做隐表面形变的稀疏体结构。隐表面把一个3D表面定义成一个体的等值面( isosurface)(或层次集)。2D等值面的一个常见例子是在地形图上画的等高线。等高线由图中海拔相同的点组成。同样,隐3D表面表示了存储在体的体素中标量值的等值面。用这种方式表示表面从数学的角度上非常方便,允许表面自由地扭曲和改变拓扑。
隐表面的高效表示使用了一个稀疏数据结构,只存储在表面附近的体素,而不是整个体,如图33-9所示。
Lefohn等(2003、2004)描述了一项基于GPU的技术,用于把隐表面从一个形状变成另一个。随着表面的演化,表示它的稀疏数据结构也要演化(因为数据结构的大小正比于隐表面的表面积)。例如,如果初始表面是一个小的球面,而最终的形态是一个大的平面,最终形态需要的存储器显然将多于初始球面需要的。本节的剩余部分将描述这个稀疏的体结构和它如何随着表面的移动而演化。
图33-9  在GPU上的动态稀疏体数据
图注:Lefohn等(2003、2004)使用基于贴面的稀疏数据结构和算法表示可形变的隐表面。隐表面的形变计算只在包含表面贴面的稀疏集上执行。随着表面的移动,新的贴面必须被分配,而且其他的必须被释放。这个算法使用GPU来计算表面的形变,并把CPU作为存储器(贴面)管理器。GPU通过把一个位向量信息发给CPU来请求贴面分配。
这个稀疏结构是通过把3D体细分成小的2D贴面建立的(参见图33-9中标记为“A”的区域)。CPU存储在GPU纹理存储器中包含这个表面的贴面(活动的贴面)(参见图33-9中标记为“B”的区域)。GPU只在活动的贴面上执行表面的形变计算(参见图33-9中的第2步)。CPU保存活动贴面的一个映射,并为GPU计算分配/释放所需的贴面。这个框架通过把CPU用作GPU的存储器管理协处理器来解决动态更新的问题。
这个系统的一个关键组件是GPU请求CPU分配或释放贴面的方式。CPU通过读取来自GPU的一个小的编码信息(图像)来接收通信(参见图33-9中的第3和4步)。每个活动的贴面在这个图像中占有一个像素,而每个像素的值是一个位码。7位中有1位用于活动的贴面,6位用于在3D体中与它相邻的贴面。每个位的值命令CPU分配或释放相关的贴面。事实上,CPU把整个图像解释成一个位向量。GPU通过在每个活动的像素上计算存储器需求来建立这个位向量图像,然后通过使用自动mipmap生成或Buck等(2004)描述的缩减技术把请求减少到每个贴面一个。一旦CPU解码了存储器请求信息,它将分配/释放请求的贴面,并把顶点和纹理坐标的新数据集传给GPU(参见图33-9的第1步)。这些顶点数据表示了贴面的新活动集,GPU在其上计算表面的形变。
总的来说,当GPU数据结构需要更新的时候,这个动态的稀疏数据结构通过向CPU传送小信息来解决需要散列功能的问题。这个结构使用了本节一开始讨论的分块策略来统一稀疏域上的计算。框架高效的理由有几条:首先,GPU-CPU通信的数量通过使用压缩的位向量信息格式来最小化;其次,CPU只担任存储器管理,让GPU执行所有“重的”计算,注意,在整个形变中,隐表面数据只存在于GPU;最后,动态的稀疏表示使计算和存储器需求能够根据隐表面的表面积而放缩,而不是它包围盒的体积。这是一个重要的优化,如果忽略了它,就会让基于CPU的实现轻松地胜过GPU版本。
本节中描述的两种动态稀疏数据结构都遵循了数据并行数据结构的规则,它们的数据元素可以并行地独立访问。Purcell等的结构是以数据并行的方式更新的,而Lefohn等的结构的一部分更新是并行的(GPU用数据并行计算产生一个存储器的分配请求),一部分是用串行程序(CPU使用栈和队列做到每次响应一个存储器请求的数组)。由于在建立可伸缩的优化算法上的重要性,复杂的GPU兼容数据结构仍然将是一个研究的活跃领域。这些数据结构是否应该完全包含在GPU里,或者使用一个混合的CPU/GPU模型将主要取决于GPU如何发展,以及CPU和GPU沟通的速度/延迟。
 
33.4  性能考虑
本节将描述一些底层细节,它们对基于GPU的数据结构的总体性能会有较大的冲击。
33.4.1  依赖的纹理读取
GPU比CPU更有优势的地方之一是能够隐藏从存储器中读取cache外的值的开销。它完成这一点的方法是发射非同步的存储器读取请求,并执行其他的、非依赖的操作来填充被存储器请求占用的时间。执行多个依赖的存储器访问(用存储器访问的结果作为下一个的地址)减少了非依赖工作的数量,因此给GPU更少的机会来隐藏存储器访问延迟。同样,如果不谨慎地使用,有些类型的依赖纹理读取会引起程序的严重减速。
不是所有的依赖纹理读取都会使程序执行速度降低。公开可用的基准工具GPUBench (Fatahalian等,2004)指出,依赖纹理读取的开销完全取决于存储器访问的cache连贯性。依赖纹理读取的危险是它们很容易会产生cache不连贯的存储器访问。尽量努力地使数据结构中的依赖纹理读取是cache连贯的。维持cache连贯的技术包括把相似的计算划分到连续的块中,是用尽可能最小的查询表,以及最小化纹理依赖的级数(由此减少不连贯访问的风险)。
33.4.2  计算频度和程序特化
33.2.2小节中描述的数据流以不同的计算频度被计算。例如,顶点流比片段流频率更低。这意味着顶点流比片段流包含更少的元素。对GPU计算有效的计算频度是常量、uniform、顶点和片段。常量值在编译期已知,uniform参数是在每个核运行时只计算一次的运行期值。可以用最低可能的频率进行计算,以利用GPU中不同的计算频度。例如,如果一个片段程序访问多个相邻的纹素,如果顶点比片段少,那么在顶点程序而不是片段程序中计算那些纹素的存储器地址通常更高效。
计算频度优化的另一个例子是优化在每个数据元素上计算相同值的核代码。在GPU核运行之前,这个计算应该在CPU上以统一的频度预计算。这样的代码在前面演示的地址变换代码(程序清单33-1、33-2和33-3)中很常见。避免这样的多余计算的方法之一是手动地预计算这些统一的结果,并适当地改变核代码(包括核的参数)。另外的,更好的选项是使用Cg编译器的程序特化功能自动地执行计算。程序特化(Jones等,1993)在“运行期常数”统一设置的参数后,在运行期重新编译程序。只依赖于这些已知值的所有代码路径都被执行,而这个常数覆盖的值就存储在编译的代码中。在Cg中得到程序特化的方法是调用cgSetParameterVariability API(NVIDIA,2004)。注意,这项技术需要重新编译核,因此只适用于要特化的核在统一的参数变化之间使用了多次的情况。这项技术通常只适用于只设了一次的统一参数(“运行期常量”)。
33.4.3  Pbuffer Survival Guide
Pbuffer(或像素缓冲区)是OpenGL中离屏的帧缓冲区。除了提供浮点帧缓冲区之外,直到最近,这些特别的帧缓冲区是惟一一种OpenGL程序员可以使用的渲染到纹理功能。但pbuffer未设计成繁重地渲染到纹理的用法,而许多GPGPU应用程序要求这一点(有时有上百个pbuffer),而且当用这种方式尝试使用pbuffer的时候,存在着许多性能陷阱。
OpenGL体系结构审评委员会(ARB)现在正致力于开发一个的新机制,用于渲染可显示的帧缓冲区之外的目标(纹理、顶点数组等)。这个扩展,与顶点和像素缓冲对象扩展相结合,将会在渲染到纹理的过程中除去pbuffer的使用。然而也要包括下面一些pbuffer的诀窍,因为很多目前的应用程序广泛地使用它们。
pbuffer的基本问题是每个pbuffer包含有自己的OpenGL渲染和设备场景。在这些场景之间切换是非常昂贵的操作。用于渲染到纹理的pbuffer传统用法把一个pbuffer和一个可渲染的纹理一起使用。结果是从一个可渲染的纹理切换到另一个需要GPU场景的切换,浪费无数的应用程序性能。本节介绍了两种技术,通过避免切换场景来极大地减少改变渲染目标的开销。
这些优化中的第一个是使用多表面pbuffer。一个pbuffer表面是pbuffer的颜色缓冲区之一(如front、back、aux 0等)。每个pbuffer表面可以拥有自己的可渲染纹理,而在表面之间交换是非常迅速的。重要的是确定不要让同一个表面既作为纹理又作为渲染目标。这是一个违法的配置,而且很可能将会导致不正确的结果,因为它破坏了流编程模型的保证——核不能写入它们的输入流中。程序清单33-7的伪代码演示如何创建和使用多表面pbuffer进行多个渲染到纹理遍。
程序清单33-7  使用OpenGL中的多表面Pbuffer进行高效的渲染到纹理
void draw( GLenum readSurface, GLenum writeSurface )
{
  // 1) 把readSurface绑定到纹理
  wglBindTexImageARB(pbuffer, readSurface);

  // 2) 把渲染目标设置成writeSurface
  glDrawBuffer(writeSurface);

  // 3) 渲染
  doRenderPass(. . .);

  // 4) 释放readSurface纹理
  wglReleaseTexImageARB(pbuffer, readSurface);
}

// 1) 分配和打开多表面pbuffer(前、后、AUX缓冲区)
Pbuffer pbuff = allocateMultiPbuffer( GL_FRONT, GL_BACK, GL_AUX0, . . .);
EnableRenderContext( pbuff );

// 2) 从FRONT读取,写入BACK
draw( WGL_FRONT_ARB, GL_BACK );

// 3) 从BACK读取,写入FRONT
draw( WGL_BACK_ARB, GL_FRONT );
第二个pbuffer优化用的是打包技术,在第33.3.1小节描述成把3D纹理展平成2D纹理。把数据存储在同一个大pbuffer的多个视区中,将会进一步避免需要切换的OpenGL环境。Goodnight等(2003)在多栅格解决方案中广泛地使用这项技术。然而,和在基本的多表面pbuffer情况中一样,必须避免同时读取和写入同一个表面。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
GPU高性能编程CUDA实战》是一本介绍GPU编程技术的实用指南。它以CUDA(Compute Unified Device Architecture)为核心,向读者展示如何利用GPU的并行计算能力来加速各种应用程序。 本书首先对GPU架构和CUDA编程模型进行了全面介绍,并讲解了CUDA的基本概念和编程原理。读者可以从中了解到GPU相对于传统CPU的优势,以及如何利用CUDA进行高效的并行编程。 接着,书中详细讲解了CUDA的编程语言和工具。读者将学会如何使用CUDA C/C++来编写并行计算的代码,并通过实际案例演示了如何调试和优化CUDA程序。此外,本书还介绍了NVIDIA的性能分析工具和CUDA GPUs的内存管理技巧,帮助读者更好地利用GPU的性能。 《GPU高性能编程CUDA实战》的一个重要特点是它提供了大量的实例代码和实战案例。通过这些案例,读者可以了解到如何用CUDA来加速图像处理、矩阵运算、深度学习等各种应用。这些示例代码都经过优化和测试,读者可以直接运行和验证,并通过它们来学习和实践CUDA编程技术。 总之,《GPU高性能编程CUDA实战》是一本很好的学习资源,特别适合对并行计算GPU编程感兴趣的开发人员和研究者。通过阅读本书,读者可以系统地了解CUDA编程的基本概念和技术,并通过实战案例来提高自己的GPU编程能力。无论是想加速现有应用程序,还是开发新的GPU应用,都能从这本书中获得很多实用的知识和经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值