提升先进OpenGL(三):Persistent-mapped Buffer

转载请注明:http://www.cnblogs.com/vertexshader/articles/Approach_zero_driver_overhead_1.html

一、引言

2014年GDC(Game Developer Conference)大会上发布了许多崭新的东西,其中有一些是和图形接口优化相关的内容,例如Microsoft提出的DirectX 12接口,在降低图形接口调用上的开销做了进一步的努力;相应的OpenGL也不甘示弱,Nvidia、AMD和Intel的成员所组成的小组也开始介绍如何减少调用图形接口方面的开销,他们表示通过接口的优化能使得OpenGL的效率提升原来的7-15倍。这篇文章的目的就是分析最新出炉的《Approach Zero Driver Overhead》的内容,我希望通过自己对于OpenGL的浅薄了解,让大家一起来看看如何去优化OpenGL的使用。

 

二、描述

OpenGL 4.x之后除了渲染管线上增加了曲面细分着色器(Tessellation Shader)和计算着色器(Compute Shader),渲染管线上并无特别明显的变化,而近些年所定义的有一些规范则是朝着接口优化的方向去努力,例如GL_ARB_multi_bind、GL_ARB_texture_storage等。图形接口调用所产生的开销,已经成为了渲染中的瓶颈所在,解决这个开销问题成为了现今优化的头等问题,比如AMD的Mantle和Microsoft的DirectX 12,都在着重解决图形接口调用带来的开销问题。谈到图形接口的进化,我联想到OpenGL的发展:从OpenGL 1.x规范刚发布的时候使用的glVertex*来设定顶点数据,到后来使用Buffer Object来设定顶点数据——图形接口的规范有一大部分是通过降低调用开销而进化的。

(一)“驱动的限制”

文章中直接提到了“驱动的限制”,表示应用程序和GPU本可以渲染更多东西的,但是因为“驱动的限制”导致了渲染的瓶颈,而这个限制则是调用图形接口时所产生的巨大开销。文章中例举了导致驱动开销的几个原因:①履行API的契约所产生的CPU消耗;②驱动中验证所产生的开销;③风险的回避(这里的风险回避指的是什么?我也没搞清楚)。

文章中也列举了OpenGL增加开销的几种情况,主要的分类则是:①同步所产生的开销;②开辟空间所产生的开销;③验证所产生的开销;④编辑所产生的开销。例如使用Buffer Object,所产生的问题则可能会集中在创建Buffer Object和更新Buffer Object时,尤其是对Buffer Object进行更新数据的时候,现今OpenGL有三种接口供更新Buffer Object:

void glBufferSubData(GLenum target,  GLintptr offset,  GLsizeiptr size,  const GLvoid * data);

void *glMapBuffer(GLenum target, GLenum access);

void *glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);

其中glBufferSubData在实践后都不推荐使用(其实这个接口在更新小量数据的时候有优势),因为其需要在Client端创建一个内存块,然后将数据从Client端复制到Server端,存在一个复制数据所产生的开销。而glMapBuffer和glMapBufferRange成为相应较为效率的更新方式,通过在Client端创建一个指针映射到Sever端的数据并且返回这个指针,应用程序可以通过这个指针来修改存储的数据,当调用glUnmap的时候,数据就更新完毕了。但是这样也存在一个问题,尤其是Uniform Buffer Object中,应用程序可能只需要更新其中的一部分,并且存在多个Uniform Buffer Object需要在渲染的时候更新;或许模型数据需要动态的修改,那么则需要每帧都进行glMapBuffer或glMapBufferRange操作对数据进行更新。两种可能的情况下,都需要对Buffer Object进行多次的Map操作,所带来的性能问题不言而喻。

还有一种情况则是OpenGL中对Object的绑定,例如Framebuffer Object、Program Object、Texture Object和Buffer Object,都会存在一种验证机制,例如Framebuffer Completeness,Shader Compilation,Mipmap Completeness,这些Object在绑定的时候都会因为验证机制导致消耗CPU时间,在某些情况下可能导致一定的性能问题。还有就是资源的绑定所产生的开销,例如将多个Texture Object绑定到Texture Unit上,以及将多个Buffer Object绑定到Uniform Buffer Object的Binding Slot上去。

为了解决这些问题,小组提供了几个可以参考的解决办法,例如使用Buffer Storage和Texture Array,以及Multi-Draw Indirect。

(二)Buffer Storage

OpenGL 4.4所提供的一个扩展就是GL_ARB_buffer_storage扩展,提供了对Buffer Object创建Immutable Storage的方法,以及对Buffer Object更加底层的控制。Buffer Object的主要用途是作为Vertex/Index Buffer Object、Pixel Buffer Object或者是Uniform Buffer Object,在文中提到如果在渲染的时候需要动态的模型所遇到的问题,也就是作为Vertex/Index Buffer Object存储数据时需要不断进行更新的问题。前面提到glMapBuffer或者glMapBufferRange是比较消耗CPU时间的(在我的机器上居然达到了30207.993μs),如果每一帧对动态的模型数据进行glMapBuffer操作之后再更新数据,一个场景中如果有多个动态模型数据,这个过程将造成大量的glMapBuffer操作,所带来的效率降低是明显的。文章中所提到的解决办法则是使用Persistent-mapped Buffer,按照字面意思就是持续不断映射的缓冲区!让我们先来细看一下Buffer Storage也就是Immutable Storage的具体细节。

GL_ARB_buffer_object提供了全新的接口来创建Immutable Storage:

void glBufferStorage(GLenum target, GLsizeiptr size, const GLvoid * data, GLbitfield flags);

接口的<target>,<size>和<data>与glBufferData的参数无异,而最大的区别就是最后<flags>,代表使用数据存储的目的,下面的列表列出了相关的参数:

标识符

内容

GL_DYNAMIC_STORAGE_BIT

允许glBufferSubData更新数据内容

GL_MAP_READ_BIT

允许客户端通过映射读取数据内容

GL_MAP_WRITE_BIT

允许客户端通过映射写入数据内容

GL_MAP_PERSISTENT_BIT

允许服务端在缓冲被映射的状态下读写数据

GL_MAP_COHERENT_BIT

客户端(服务端)更新的数据会在服务端(客户端)立即可见的

GL_CLIENT_STORAGE_BIT

指定数据在客户端存储

相应的,通过这个接口创建Buffer Object之后,其Buffer Object State略有所不同:

状态名

glBufferData的值

glBufferStorage的值

GL_BUFFER_SIZE

<size>

<size>

GL_BUFFER_USAGE

<usage>

GL_DYNAMIC_DRAW

GL_BUFFER_ACCESS

GL_READ_WRITE

GL_READ_WRITE

GL_BUFFER_ACCESS_FLAGS

0

0

GL_BUFFER_IMMUTABLE_STORAGE

GL_FALSE

GL_TRUE

GL_BUFFER_MAPPED

GL_FALSE

GL_FALSE

GL_BUFFER_MAP_POINTER

NULL

NULL

GL_BUFFER_MAP_OFFSET

0

0

GL_BUFFER_MAP_LENGTH

0

0

GL_BUFFER_STORAGE_FLAGS

GL_MAP_READ_BIT

 | GL_MAP_WRITE_BIT 

| GL_DYNAMIC_STORAGE

<flags>

其中两个重要的参数便是GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT,GL_MAP_PERSISTENT_BIT的意义是当Buffer Object中的GL_BUFFER_MAPEED状态是GL_TRUE时也就是Buffer Object被映射的时候,也可以被服务端读写数据,也就是延长了Map Pointer的时效。更加详细地说就是,以前当Buffer Object处于Mapped状态的时候,其只能进行客户端的读写操作,而不能进行其他的操作例如glDrawElements这样的绘制命令;现在通过Immutable Storage设置GL_MAP_PERSISTENT_BIT位,数据可以在Mapped状态时也可以被服务端读写了,可以调用glDrawElements或者glCopyBufferSubData,而不会产生任何的错误。GL_MAP_COHERENT_BIT代表的是当Buffer Object被Map的时候,不管是客户端和服务端的哪一端更新了数据,更新的数据都会立即在另一端可见,其和GL_MAP_PERSISTENT_BIT一般是联合起来一起使用的。这就好像是把Buffer Object变成了一般的“数组”一样。

文章中提到的Persistent-mapped Buffer就是<flags>设置为GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT的Immutable Buffer Storage,其减少驱动开销的办法是在创建Buffer Object之后就使用glMapBufferRange获得Map Pointer并存储起来,因为Map Pointer的持久有效性,程序只需要glMapBufferRange一次以后只要再通过这个指针更新数据就好,而不需要因为其他接口不允许Mapped状态而使用glUnmapBuffer弃用指针,然后下次更新再次获得指针的办法,这样每帧调用glMapBufferRange的开销直接就被消除了(其实我觉得奇怪的地方就是,如果没有使用Immutable Storage,在Map之后获得指针后调用glUnmapBuffer,其指针还是有效的并且还可以更新数据,在Nvidia、AMD和Intel的显卡上都是如此,貌似glUnmapBuffer只是简单的更改了GL_BUFFER_MAPPED状态,虽然在规范中提到千万不要在glUnmapBuffer之后用这个指针)。代码范例如下:

// 创建Persistent-mapped buffer对象

glGenBuffers(1, &buf);

glBindBuffer(GL_ARRAY_BUFFER, buf);

 

GLbitfield flags= access_bit 

                   | GL_MAP_PERSISTENT_BIT // 处在Mapped状态也能使用

                   | GL_MAP_COHERENT_BIT; // 数据对GPU立即可见

 

glBufferStorage(GL_ARRAY_BUFFER, size, data, flags;

// 只需要Map一次就好,以后保存这个指针用于更新,不需要再Map

GLvoid *ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, size, flags);

 

// 绘制过程

// 通过<ptr>直接更新数据

UpdateBufferObject(ptr);

 

// 设置渲染需要的数据,然后直接调用Draw Call,不需要glUnmapBuffer

glDrawElements(...);

这个GL_MAP_PERSISTENT不仅仅影响的是Draw Call,其影响的接口非常之多,影响的内容都是Buffer Object在Mapped状态也可以被使用,可以分为如下几种情况:

影响的情况

影响的接口

Pixel Buffer Object相关

glReadPixels glCompressedTexSubImage* 

glGetCompressedTexImage 

glTexSubImage* 

glGetTexImage

Buffer Object相关

glBufferSubData

glClearBufferSubData

glClearBufferData

glInvalidBufferSubData

glInvalidBufferData

glCopyBufferSubData

Draw Call相关

glDrawArrays

glDrawArraysIndirect

glDrawArraysInstanced

glDrawArrayInstancedBaseInstanced

glDrawElements

glDrawElementsBaseVertex

glDrawElementsIndirect

glDrawElementsInstanced

glDrawElementsInstancedBaseInstanced

glDrawElementsInstancedBaseVertex

glDrawElementsInstancedBaseVertexInstance

glDrawRangeElements

glDrawRangeElementsBaseVertex

glDrawTransformFeedback,

glDrawTransformFeedbackInstanced

glDrawTransformFeedbackStream

glDrawTransformFeedbackStreamInstanced

通过这个扩展,可以想到不管是创建什么Buffer Object,是Index/Vertex Buffer当做顶点数据来源,还是Pixel Buffer Object作为纹理的像素数据来源,还是Uniform Buffer Object然后作为Shader Program的数据来源,可以想到对于动态的Index/Vertex Buffer Object或者是动态的Uniform Buffer Object,应当首先设置GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT位,然后首先进行glMapBufferRange操作获得指针然后保存,日后更新只需要使用这个指针即可。在每帧中,动态模型的数据更新,或者是有很多纹理更新还有就是着色器参数的更新,都能抛弃过多的glMapBufferRange操作,从而减少每帧的耗时(glCopyBufferSubData所产生的Server端的数据更新可能没法立即更新到Map Pointer,可以使用glFinish()来强制完成所有的命令)!对于静态的Index/Vertex Buffer和Uniform Buffer Object,尽可能使用原来的方法;而对于Persistent-mapped buffer,要尽可能的减少Copy操作(会带来同步的问题)。

posted @ 2014-03-25 15:18 YangZhao1992 阅读(...) 评论(...) 编辑 收藏