在过去的几年里,硬件加速流水线获得了无与伦比的发展.同时也日愈复杂,它们的性能特征越来越难以理解.以前改善性能只意味着减少用于渲染的CPU内循环周期,现在变成确定瓶颈与系统的处理它们.此时优化性能的的主导思想是,流水线的速度由最慢的一个阶段决定.本文中讨论了现代显卡所存在的瓶颈之一,以及部分的解决办法.
数据在主板中的传递
CPU从RAM中获得数据并且处理数据,然后写入RAM中.GPU的工作方式类似于CPU,从VRAM中获得数据处理数据,然后写入VRAM中并在屏幕上显示出来.
作为GPU流水线的第一步,系统通过AGP或PCI Express总线把顶点和索引传送到GPU或局部桢缓冲内存中.虽然现在的AGP bus比起让PCI bus来负责传递数据的确快了不少,但比起RAM和CPU或VRAM与GPU之间的速度来讲,仍显不足.当数据传输的不即时而造成了CPU与GPU在等待中耗费时钟或空转的时候,就可以确定瓶颈出在BUS之上.
一般的检查方式是通过改变顶点格式大小,来确定得到数据是否成为了应用程序的瓶颈.
由于瓶颈出在硬件构架方式上,很难得到一个真正意义上完美的解决方案.下面是几个可以适当提升效果的技巧.
减少资源锁定,即尽量避免访问渲染期间GPU正在使用的资源
最大化一次性传入数据
从OpenGL简单的来讲就是避免使用glVertex之类,转而使用glDrawArray,对数据集进行批次传送.在这里它的作用不仅仅只是优化数据传递带宽需求,它更可以减少函数调用在系统当中的消耗(在某些系统下,消耗相当可观).
同时使用纹理atlas方法.因为在不同的物体使用不同的纹理时,批传送往往被打破.通过把许多纹理安排进单个纹理当中,并适当的设定纹理坐标,就能在单个绘画调用中发送使用多个纹理的几何体.
减少顶点传递开销
在顶点格式中使用适当的位,足够就可以了.不需要对一切都使用浮点格式.使用索引的时候,用16位代替32位.因为它更容易查找,移动起来更轻松并且占用更少的内存.
由于顶点数据可以在现代GPU中实现高速缓存(下面将讨论的方法),在任意的内存层次当中以相对连续的方式访问顶点数据,引用的空间局部性有助于最大化高速缓冲储存器的命中率,减少对带宽的要求.
顶点缓存
相对于上述一些技巧性的优化,使用现代显卡所支持的这一特性可以更有效的减低对数据传递带宽的需求.
由于OpenGL的实现特性,数据被储存在系统内存里.在渲染过程中每次需要使用被启用的数组时,数据就从内存传输到显存.为了处理这种情况OpenGL1.5中引入了缓冲区对象,允许把数据储存在VRAM中,并且只传输一次.
利用缓冲区对象储存顶点数据分为以下几个步骤.
①创建缓冲区对象.
void glGenBuffers(GLsizei n, GLuint * buffers);
该函数通过buffers返回n个未被使用的缓冲区表识符(u int).
GLboolean glIsBuffers(GLuint buffer);
确定buffer是否为已经被绑定的缓冲区表识符.
②绑定缓冲区对象.
void glBindBuffer(GLenum target, GLuint buffer);
target为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY_BUFFER之一.此函数有三种不同的行为模式.
当buffer首次使用时,就创建一个缓冲区,并绑定buffer作为其表识符.当绑定到一个以前创建的缓冲区时,这个缓冲区变成为当前活动的对象.当绑定buffer值为0时,停止使用缓冲区对象.
③使用顶点数据分配初始化缓冲区对象.
void glBufferData(GLenum target, GLsizeiptr size,const GLvoid * data,GLenum usage);
target同上,size表示分配在显存当中的储存单位个数.data是一个指向内存的指针,用于初始化缓冲区对象.usage提示数据在分配之后如何进行度取和写入,根据所指定的值OpenGL实现可能会对其进行针对性的优化,它有以下几种可能的值:
GL_STREAM_DRAW,GL_STREAM_READ,GL_STREAM_COPY,
GL_STATIC_DRAW,GL_STATIC_READ,GL_STAIC_COPY,
GL_DYNAMIC_DRAW,GL_DYNAMIC_READ,GL_DYNAMIC_COPY.
Draw-数据作为顶点数据,用于渲染.
Read-数据从一个OpenGL缓冲区(桢缓冲区之类的)读取,并在程序中与渲染并不直接相关的各种计算过程中使用.Copy-数据从一个OpenGL缓冲区读取,然后作为顶点数据,用于渲染.
Stream-缓冲区的对象需要时常更新,但使用次数很少.
Static-只需要一次指定缓冲区对象中的数据,但使用次数很多.
Dynamic-数据不仅需要时常更新,使用次数也很多.
④更新缓冲区对象内的数据
更新数据有以下两种方式:
void glBufferSubData(GLenum target,GLintptr offset,GLsizeiptr size,const GLvoid * data);
target同上.由于顶点数组数据的所有格式在缓冲区对象内同样有效.所以顶点\颜色\法线等相关数据都可以放入缓冲区中,所以需要指定一个offset作为缓冲区对象中数据的偏移量.size为起始下标.data 指向更新的数据.
另一种:
GLvoid * glMapBuffer(GLenum target,GLenum access);
target同上.access为访问数据的方式,可以为以下几个值:GL_READ_ONLY,GL_WRITE,GL_READ_WRITE.
这个函数直接获得一个指向被绑定缓冲区内数据的指针,通过给值的方式读写缓冲区对象.在读写完毕之后调用GLboolean glUnmapBuffer(GLenum target);表示被绑定的缓冲区对象更新完成,并且可以释放.
⑤清除缓冲区对象
void glDeleteBuffers(GLsizei n,const GLuint * buffers);
删除n个缓冲区对象,由buffer表识符数组指定.