ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

再谈vbo

2021-05-20 08:02:37  阅读:216  来源: 互联网

标签:glNamedBufferStorage buffer object vbo 再谈 usage GL


我们之前都是通过glNamedBufferData初始化buffer object,初始化的意思是为buffer object开辟显存空间,并填充数据:

GLfloat position[] =
{
    -1.0f, -1.0f,
    0.0f, 1.0f,
    1.0f,  -1.0f,
};
GLuint vbo = 0;
glCreateBuffers(1, &vbo);
glNamedBufferData(vbo, sizeof(position), position, GL_STATIC_DRAW);

...
drawTriangle();

glNamedBufferData一个比较方便的地方就是,如果我们在drawTriangle()完成之后还想再绘制一个方形,可以复用这个vbo。

...		//after drawing triangle
GLfloat position[] =
{
    -0.5f, 0.5f,
    -0.5f, -0.5f,
     0.5f,  0.5f,
     0.5f, -0.5f,
};

glNamedBufferData(vbo, 8 * sizeof(GLfloat), position, GL_STATIC_DRAW);	//只能等drawTriangle完成之后才能执行

...
drawRect();

一个buffer object可以复用很多次,但是不推荐这么做。因为这样会降低GPU的并行力度,起码我们可以推断:glNamedBufferData(vbo, 8 * sizeof(GLfloat), position, GL_STATIC_DRAW)要等待drawTriangle()完成之后才能执行。有经验的OpenGL程序员都会创建两个VBO完成这两次render操作。

但是我想表达的是:对于同一个buffer object,可以多次调用glNamedBufferData为它重新分配空间和指定usage。

那有没有一种API,一旦初始化完buffer object之后,其size和usage就不得发生改变呢?有,它就是我们今天要讲的glNamedBufferStorage

glNamedBufferStorage

先看一下这个命令的原型:

void glNamedBufferStorage(GLuint buffer,
 	GLsizeiptr size,
 	const void *data,
 	GLbitfield flags);

前三个参数与glNamedBufferData相同,最后一个参数是以位组合来表达此buffer的usage,它的值可能是以下标识位组合:

  • GL_DYNAMIC_STORAGE_BIT
  • GL_MAP_READ_BIT
  • GL_MAP_WRITE_BIT
  • GL_MAP_PERSISTENT_BIT
  • GL_MAP_COHERENT_BIT

我不会去机械的罗列出这些标识位的意思,因为那样会很无趣。我的理念是用到哪个东西,再讲哪个东西。

首先,再次明确一下,官方的说法是:一旦用glNamedBufferStorage为buffer object分配好空间并指定usage flag之后,该buffer object的size和usage flag就不能再发生改变,不过数据的内容倒是可以改变。

在我看来,这句话说的实在是太轻了,因为哪怕是这样代码:

//以相同的参数调用glNamedBufferStorage两次
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);

OpenGL都会抱怨产生了一个错误:Cannot modify immutable buffer。

哇,您还不如和我说对于同一个buffer object,只能调用一次glNamedBufferStorage呢。不过这样的严格限制,带来的好处是:比起glNamedBufferDataglNamedBufferStorage能够带来更好的性能。而且glNamedBufferStorage的usage flag参数更加现代化,准确的判断usage flag参数,对于性能的优化以及程序的正确执行都至关重要。比如你调用了glNamedBufferStorage为某个buffer object开辟了空间并初始化了其中的数据,之后你再也不想对这个buffer object进行读取或者写入,那么就可以把usage flag设置为0,这将带来最好的性能优化。

但是如果我们还想修改这个buffer object中的数据,或者是想把buffer object中的数据读取至内存,该怎么办呢?

读取buffer中的数据

在把buffer object的usage设置为0的情况下,我们可以调用glNamedBufferSubData来读取buffer中的数据:

glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);	//最后一个参数设置为0
...
glGetNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), positionPointer);

这样,vbo中的数据就会从显存映射到positionPointer指向的内存空间。

不过这里还是有一个小细节:调用glNamedBufferStorage之后,OpenGL告诉我这个buffer object的usage hint是GL_STATIC_DRAW,然后我们在某个时机再调用glGetNamedBufferSubData之后,OpenGL就会告诉我这个buffer object的usage hint是GL_DYNAMIC_DRAW。我们都知道GL_STATIC_DRAW是要比GL_DYNAMIC_DRAW的性能更好。

向buffer中写入数据

我们可以调用glNamedBufferSubData来向buffer object中写入数据:

glNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), newTrianglePoints);

不过,初始化buffer object的时候,要把usage设置为GL_DYNAMIC_STORAGE_BIT

glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, GL_DYNAMIC_STORAGE_BIT);

而且,与"读取buffer中的数据"中得到的结论一致:调用glNamedBufferSubData之后,OpenGL也会告诉我buffer object的usage hint是GL_DYNAMIC_DRAW。

所以各位同学,哥哥奉劝你们:使用glNamedBufferStorage初始化buffer object空间之后,尽量就不要再对buffer object进行读写了吧。

我尝试过把usage设置为0,然后向buffer写入数据:

glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);
...
glNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), newTrianglePoints);

OpenGL又告诉我发生了一个错误: Buffer contents cannot be modified because the buffer was created without the GL_DYNAMIC_STORAGE_BIT set。虽然尽可能精简usage flag是一个好习惯,不过前提还是要保证程序的正确性。

另外,虽然glNamedBufferSubData看起来与glNamedBufferData有点像,但是它们确实不是同一类API。

glNamedBufferDataglNamedBufferStorage算是同一类API,而glNamedBufferSubData是配合glNamedBufferStorage使用的。

glMapNamedBufferRange

在早期的OpenGL,我们可以使用glMapBufferglUnmapBuffer来读写buffer object的数据。modern OpenGL有更为高级的API来做这件事:

void *glMapNamedBufferRange(GLuint buffer,
 	GLintptr offset,
 	GLsizeiptr length,
 	GLbitfield access);

此君与glMapNamedBuffer相比,有两个优点:

  1. 能够将buffer object的一部分(而非全部)数据映射到内存空间。
  2. 能够以位组合的形式描述访问策略,access参数必须与glNamedBufferStorageusage配合起来使用。

比如我想读取三角形的顶点位置buffer中的数据,然后进行射线求交:

GLfloat position[] =
{
    ...
};
glNamedBufferStorage(vbo, 9 * sizeof(GLfloat), position, GL_MAP_READ_BIT);	//初始化buffer时,usage flag要包含GL_MAP_READ_BIT才能读取buffer的内容

...

GLfloat* trianglePosition = (GLfloat*)glMapNamedBufferRange(vbo, 0, 9 * sizeof(GLfloat), GL_MAP_READ_BIT);//最后一个参数表示仅对buffer进行读操作

//射线求交的伪代码
Ray ray;
ray.intersectWithTriangle(trianglePosition);

GLboolean unMap = glUnmapNamedBuffer(vbo);	//操作完成,告诉OpenGL我不会再使用trianglePosition指针指向的内容了
assert(unMap);

需要反复强调的:glNamedBufferStorage的usage flag参数要与glMapNamedBufferRange的access flag参数配合起来。

另外,当不再需要glMapNamedBufferRange返回的数据之后,应尽快进行glUnmapNamedBuffer。然后检查其返回值,在某些极端的情况下,可能会unmap失败。

当我进行读操作的时候,OpenGL没有通知我关于buffer的usage hint的消息。当我进行写操作的时候,OpenGL通知我此buffer的usage hint为GL_DYNAMIC_DRAW。

到底该使用哪个

既然glNamedBufferSubData(glGetNamedBufferSubData)glMapNamedBufferRange都能读写buffer的数据,那问题来了,我们应该用哪个呢?

首先,glMapNamedBufferRange更具性能优势。我不去讲什么专业名词(譬如什么asynchronous DMA transfer之类的东东),仅从代码的角度来分析为什么glMapNamedBufferRange速度更快。

首先,我们为glNamedBufferSubData传入的指针所指向的内存空间是我们自己分配出来的。譬如:

GLfloat* trangleColor = new GLfloat[9];
for (int i = 0; i < 3; ++i)
{
    for (int j = 0; j < 3; ++j)
	trangleColor[i * 3 + j] = 1.0f;
}
glNamedBufferSubData(vbo, 0, sizeof(GLfloat) * 9, trangleColor);
delete[] trangleColor;		//传输到buffer之后直接销毁

如上代码,OpenGL无法预料到开发者在调用完glNamedBufferSubData之后,怎么处理trangleColor指向的内存空间,比如我调用完glNamedBufferSubData之后直接销毁掉了这部分数据。所以OpenGL只能让CPU在glNamedBufferSubData先停下来,集中精力把triangleColor指向的内存空间的内容拷贝到显存空间,等glNamedBufferSubData返回之后(拷贝完成),才能继续往下执行。

glMapNamedBufferRange返回内存空间是OpenGL自己管理的,当我们调用glUnmapNamedBuffer之后,可以立即返回,然后在比较闲的时候偷偷的执行拷贝操作,并且这个操作也可以与其它的命令并行执行。

所以如果只是很偶尔地读写的很小的数据量,两者区别可能没有那么明显。但是如果频繁的读写,或者一次读写的数据量很大,那么glMapNamedBufferRange glUnmapNamedBuffer的性能优势就非常明显了。

glMapNamedBufferRange还有一个优点,比如三角形的顶点数据存放到了磁盘的某个文件中,现在要用这个文件的顶点来创建vbo:

//使用glNamedBufferSubData
FILE* f = fopen("position.dat", "rb");
fseek(f, 0, SEEK_END);
long fileSize = std::ftell(f);
fseek(f, 0, SEEK_SET);

glNamedBufferStorage(vbo[0], fileSize, nullptr, GL_DYNAMIC_STORAGE_BIT);

GLchar* position = new GLchar[fileSize];	//申请内存空间用来存放文件的顶点数据
fread(position, 1, fileSize, f);
glNamedBufferSubData(vbo[0], 0, fileSize, position);
delete[] position;
fclose(f);
//使用glMapNamedBufferRange
FILE* f = fopen("position.dat", "rb");
fseek(f, 0, SEEK_END);
long fileSize = std::ftell(f);
fseek(f, 0, SEEK_SET);

glNamedBufferStorage(vbo[0], fileSize, nullptr, GL_MAP_WRITE_BIT);

void* data = glMapNamedBufferRange(vbo[0], 0, fileSize, GL_MAP_WRITE_BIT);
fread(data, 1, fileSize, f);
fclose(f);
glUnmapNamedBuffer(vbo[0]);

利用glMapNamedBufferRange就可以避免先把文件的内容拷贝到内存中这一步骤。

小结

  1. 推荐使用glNamedBufferStorage初始化buffer object。
  2. 推荐使用glMapNamedBufferRange glUnmapNamedBuffer读写buffer中的数据。

标签:glNamedBufferStorage,buffer,object,vbo,再谈,usage,GL
来源: https://www.cnblogs.com/howelldong/p/14788346.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有