再谈vbo
我们之前都是通过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
呢。不过这样的严格限制,带来的好处是:比起glNamedBufferData
,glNamedBufferStorage
能够带来更好的性能。而且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。
glNamedBufferData
和glNamedBufferStorage
算是同一类API,而glNamedBufferSubData
是配合glNamedBufferStorage
使用的。
glMapNamedBufferRange
在早期的OpenGL,我们可以使用glMapBuffer
和glUnmapBuffer
来读写buffer object的数据。modern OpenGL有更为高级的API来做这件事:
void *glMapNamedBufferRange(GLuint buffer,
GLintptr offset,
GLsizeiptr length,
GLbitfield access);
此君与glMapNamedBuffer
相比,有两个优点:
- 能够将buffer object的一部分(而非全部)数据映射到内存空间。
- 能够以位组合的形式描述访问策略,
access
参数必须与glNamedBufferStorage
的usage
配合起来使用。
比如我想读取三角形的顶点位置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
就可以避免先把文件的内容拷贝到内存中这一步骤。
小结
- 推荐使用
glNamedBufferStorage
初始化buffer object。 - 推荐使用
glMapNamedBufferRange glUnmapNamedBuffer
读写buffer中的数据。
再谈vbo的更多相关文章
- Unity教程之再谈Unity中的优化技术
这是从 Unity教程之再谈Unity中的优化技术 这篇文章里提取出来的一部分,这篇文章让我学到了挺多可能我应该知道却还没知道的知识,写的挺好的 优化几何体 这一步主要是为了针对性能瓶颈中的”顶点 ...
- [转载]再谈百度:KPI、无人机,以及一个必须给父母看的案例
[转载]再谈百度:KPI.无人机,以及一个必须给父母看的案例 发表于 2016-03-15 | 0 Comments | 阅读次数 33 原文: 再谈百度:KPI.无人机,以及一个必须 ...
- Support Vector Machine (3) : 再谈泛化误差(Generalization Error)
目录 Support Vector Machine (1) : 简单SVM原理 Support Vector Machine (2) : Sequential Minimal Optimization ...
- 浅谈HTTP中Get与Post的区别/HTTP协议与HTML表单(再谈GET与POST的区别)
HTTP协议与HTML表单(再谈GET与POST的区别) GET方式在request-line中传送数据:POST方式在request-line及request-body中均可以传送数据. http: ...
- Another Look at Events(再谈Events)
转载:http://www.qtcn.org/bbs/simple/?t31383.html Another Look at Events(再谈Events) 最近在学习Qt事件处理的时候发现一篇很不 ...
- C++ Primer 学习笔记_32_STL实践与分析(6) --再谈string类型(下)
STL实践与分析 --再谈string类型(下) 四.string类型的查找操作 string类型提供了6种查找函数,每种函数以不同形式的find命名.这些操作所有返回string::size_typ ...
- 再谈JSON -json定义及数据类型
再谈json 近期在项目中使用到了highcharts ,highstock做了一些统计分析.使用jQuery ajax那就不得不使用json, 可是在使用过程中也出现了非常多的疑惑,比方说,什么情况 ...
- C++ Primer 学习笔记_44_STL实践与分析(18)--再谈迭代器【下】
STL实践与分析 --再谈迭代器[下] 三.反向迭代器[续:习题] //P355 习题11.19 int main() { vector<int> iVec; for (vector< ...
- C++ Primer 学习笔记_43_STL实践与分析(17)--再谈迭代器【中】
STL实践与分析 --再谈迭代器[中] 二.iostream迭代[续] 3.ostream_iterator对象和ostream_iterator对象的使用 能够使用ostream_iterator对 ...
随机推荐
- 翻译:《实用的Python编程》06_02_Customizing_iteration
目录 | 上一节 (6.1 迭代协议) | 下一节 (6.3 生产者/消费者) 6.2 自定义迭代 本节探究如何使用生成器函数自定义迭代. 问题 假设你想要自定义迭代模式. 例如:倒数: >&g ...
- 鸿蒙运行报错:Failure[INSTALL_PARSE_FAILED_USESDK_ERROR] Error while Deploying HAP
问题描述 近期,使用DevEco-Studio新建手机类型的工程,编译成功,发布到模拟器(鸿蒙P40)时出错,如下图: 原因分析 本地DevEco-Studio使用的SDK版本与设备(P40)不匹配导 ...
- 计算机体系结构——CH5 标量处理机
计算机体系结构--CH5 标量处理机 右键点击查看图像,查看清晰图像 X-mind 计算机体系结构--CH5 标量处理机 先行控制技术 指令得重叠执行方式 顺序执行方式 一次重叠执行方式 二次重叠技术 ...
- 文件查找工具 find 详解(附:生产示例)
1. 文件查找:在文件系统上查找符合条件的文件 命令 解释 which 查看可执行文件的位置,只能寻找执行文件,并在PATH变量里面寻找 whereis 查看文件的位置:只能查二进制文件,说明文档,源 ...
- Distributed | Raft
1. 复制状态机 一致性算法是在复制状态机的背景下产生的.在这种方法下,一组服务器的状态机计算相同状态的相同副本,即使某些服务器宕机,也可以继续运行. 复制状态机通常使用复制日志实现,每个服务器存储一 ...
- 安装Dynamics CRM Report出错处理一
删除下面两个注册表项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce.HKEY_CURRENT_USER\So ...
- (九)Struts2模型驱动和属性驱动
出于结构清晰的考虑,应该采用单独的Model实例来封装请求参数和处理结果,这就是所谓的模型驱动, 所谓模型驱动,就是使用单独的JavaBean来贯穿整个MVC流程. 所谓属性驱动,就是使用属性来作为贯 ...
- 测开新手:从0到1,自动化测试接入Jenkins学习
大家好,我叫董鑫,一个在测试开发道路上的新手,之前一直从事手工功能测试,前段时间抽空又温习了一遍老师全栈测开训练营中自动化测试.CICD的知识,最近公司正好有一个项目可以实践练手,趁热打铁,将自动化测 ...
- windows利器使用与配置
1 概述 这篇文章主要讲述了一些windows下的"利器"级别工具的使用以及配置. 2 Listary Listary是一款强大的搜索工具,可以快速搜索过滤各种文件. 点击这里下载 ...
- Vue学习笔记(三)
1 监听 在Vue.js中可以通过watch来监听数据的变化,比如通过watch实现的简单计数器: <div id="app"> <p>计数器:{{coun ...