OpenGL顶点数据传输速度优化

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/you_lan_hai/article/details/50994121

《OpenGL顶点数据传输速度优化》
作者: 游蓝海
文章链接:http://blog.csdn.net/you_lan_hai/article/details/50994121
转载请注明出处

前言

最近在给cocos2d-x v2.x的一个项目做渲染优化,执行渲染批处理(Batch)的时候,发现顶点数据传输速度很慢,实在是颠覆了我的OpenGL认知。
常规的Batch原理:
· 将渲染命令加入到一个缓冲区当中
· 根据需求对渲染命令进行排序
· 合并所有命令的顶点数据
· 提交顶点数据
· 根据不同材质分批次渲染

传输顶点数据用到的两个API:

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

glBufferData会分配一块新的内存,供程序存放数据。glBufferSubData会使用之前已经分配过的内存来存放数据。

问题

我事先创建了一个顶点buffer和索引buffer,索引buffer不用多说,其数据是一次就计算完成的,之后就不用修改了。而顶点数据每帧都会变化,所以需要动态的更新。

我在创建顶点buffer的时候,使用glBufferData分配足够大的空间。然后每次提交顶点数据的时候,使用glBufferSubData,仅提交实际使用到的数据。理论上来说,这样不会引发额外的内存分配行为,传输效率应该会很高。伪代码如下:

void init()
{
    glGenBuffer(...);
    glBindBuffer(...);
    //分配一块很大的空间
    glBufferData(GL_ARRAY_BUFFER, 32 * 1024, NULL, GL_DYNAMIC); 
    ...
}
void draw()
{
    for(int k = 0; k < parts.size(); ++k)
    {
        Part *p = parts[i];
        //仅提交用到的数据。可能只有几百字节,或者几k。
        glBufferSubData(GL_ARRAY_BUFFER, 0, p->vertices.size(), p->vertices.data());
        for(int i = 0; i < p->commands.size(); ++i)
        {
            drawCommand(p->commands[i]);
        }
    }
}

实测之后发现,掉帧特别明显,glBufferSubData操作相当耗时。百思不得其解,就查看了cocos2d-x v3.x渲染部分的代码,发现他每次提交顶点数据的时候,都是直接使用glBufferData分配空间,而且他们刻意的注释掉了使用glBufferSubData的代码。这让我更迷惑了,感觉自己的理论彻底被颠覆了。

原因

接着就去google上寻找原因,看到有人推荐看一下《OpenGL Insights》渲染效率优化部分的文章,文章全是英文,看的不是完全懂。大致理解如下:

所有的OpenGL操作,并不会被立即同步到显卡,而是被缓存在本地,等到合适的时机(比如swapBuffer)批量传输到显卡。对于顶点数据,会使用DMA进行异步传输,并不会阻塞CPU。

那么问题就来了,如果现在顶点buffer的数据还没有传输完成,我们立马又要向顶点buffer写数据了,这会出现什么情况?比如,如下的操作,写数据与渲染交替进行:

glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, datas);
glDrawElements(GL_TRIANGLES, n, GL_UNSIGNED_SHORT);
glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, datas);
glDrawElements(GL_TRIANGLES, n, GL_UNSIGNED_SHORT);
...

结论是,CPU还是会阻塞,直到DMA把顶点buffer中的数据传输完毕后,才允许我们继续向里面写数据。即便是有DMA异步传输,碰到不良的代码,还是会发生同步等待。

解决办法

书中说到,可以使用多个顶点buffer交替进行,或者使用顶点buffer的不同位置,都可以达到避免阻塞的作用。
当然,还有一种简单方法,我们也可以调用glBufferData,来强制OpenGL在本地分配一块新的内存,供我们存贮新数据。这样DMA会继续传输旧内存地址上的数据,然后再传输新内存地址上的数据,这种方式下CPU也就不用等待DMA了。虽然浪费了额外的内存空间,但是提高了数据传输的吞吐量,还是值得的。
总结来说,OpenGL的操作是异步的,我们要避免同步操作所引发的阻塞行为。

更多阅读

  1. 顶点数据传输优化: https://www.opengl.org/wiki/Buffer_Object_Streaming
  2. 《OpenGL Insights》第28章:http://download.csdn.net/download/you_lan_hai/9787675

猜你喜欢

转载自blog.csdn.net/you_lan_hai/article/details/50994121