▲点击上方“CocoaChina”关注即可免费学习 iOS 开发
作者:熊皮皮
原文链接:http://www.jianshu.com/p/0b66c00d7073
本文档的任务是使用OpenGL ES 3接口,实现一个简单的读取经GPU处理的数据的程序,描述Transform Feedback的使用,方便后续学习粒子效果、图像处理等新内容。简洁起见,后续将OpenGL缩写为GL,OpenGL ES缩写为ES。关于Transform Feedback的具体应用可查看Soft Kitty OpenGL ES 3.0 demo,效果如下所示。完整演示视频在Youtube。
特别说明,OpenGL桌面版program允许只有一个vertex shader,不设置fragment shader可正常工作。然而,ES 3.0规定program必须搭配一个vertex shader和fragment shader,哪怕它们是空操作,否则链接阶段异常。
Transform Feedback的一个优势是把顶点着色器处理后的数据写回顶点缓冲区对象(Vertex Buffer Objects, VBOs),避免GPU、CPU内存之间往返拷贝数据,节省了时间。由于iOS使用统一内存模型,GPU、CPU数据实际都在存储在主存,而Android不一定,如Nexus 6P,映射GPU地址到CPU,经我们团队测试,大约消耗20ms。
1、算法
iOS、Android都遵循如下算法,不同的是ES与本地窗口(比如,UIKit)桥接的配置。
配置EGL上下文
提供用于计算的顶点着色器和片元着色器
配置program
在链接program前配置transform feedback输出属性名
链接program
配置GPU的输入数据缓冲区并上传数据至GPU
配置GPU的输出数据缓冲区并与transform feedback绑定
禁用光栅化等渲染管线后续操作
进入transform feedback模式
绘图调用
结束transform feedback模式
同步GPU
映射GPU内存
读取GPU计算结果
解除映射
操作1~8可放在初始化函数中,后续步骤开始渲染操作。经测试,在iOS上使用Transform Feedback必须配合GLKViewController。初始化过程使用的上下文信息,在栈或堆中声明并不影响操作结果。若有更复杂操作,如配置GPUImage,则应保存EAGLContext,在正常的线程上设置上下文。
2、示例实现
1、配置EGL上下文,参考我另一个文档iOS OpenGL ES 3 编程 1:"Hello world"。简单起见,在此创建Game类型项目,在GLKViewController子类中编写后续代码。自行派生GLKViewController需正确配置GLKView,可能遇到的情况已在3.1节GLKit不使用GLView无效描述。
2、提供用于计算的顶点着色器和片元着色器。由于只做计算,不显示画面,那么Vertex Shader才是真正工作的地方。
#version 300 eslayout(location = 0) in float inValue;
out float outValue;void main(){
outValue = sqrt(inValue);
}
Fragment Shader是空操作。
#version 300 es
void main()
{
}
我们使用GPU是想利用它的并行特性,那么,就iPad Air 2而言,有多少个统一并行计算单元呢?这个无法用OpenGL ES接口查询,需查找相应的芯片手册,下面这段查询代码输出无效,在此作只示例。
#import //////////////////////////////////////////////////////printf("%s\n", glGetString(GL_VERSION));
GLint vertexUnits;
glGetIntegerv(GL_MAX_VERTEX_UNITS_OES, &vertexUnits);
执行结果:
OpenGL ES-CM 1.1 Apple A8X GPU - 77.14vertex units = 4
3、配置program。注意不要马上链接program。
GLuint vertShader, fragShader;
NSString *vertShaderPathname, *fragShaderPathname;
// Create shader program.
_program = glCreateProgram();
// Create and compile vertex shader.
vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];
if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) {
NSLog(@"Failed to compile vertex shader");
return NO;
}
// Create and compile fragment shader.
fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"];
if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) {
NSLog(@"Failed to compile fragment shader");
return NO;
}
// Attach vertex shader to program.
glAttachShader(_program, vertShader);
// Attach fragment shader to program.
glAttachShader(_program, fragShader);
4、在链接program前配置transform feedback输出属性名。
仔细观察,本文使用的Vertex Shader与正常绘图所用的着色器略有区别:没输出顶点坐标给Fragment Shader使用。因此,需要glTransformFeedbackVaryings告诉ES欲捕获到输出缓冲区的属性属性。
GLchar *varyings[] = {"outValue"};
glTransformFeedbackVaryings(_program, sizeof(varyings) / sizeof(varyings[0]), varyings, GL_INTERLEAVED_ATTRIBS);
glTransformFeedbackVaryings需要输出变量的数量及名称,在varyings数组指定Vertex Shader将输出的属性名。
5、链接program。链接操作包含检查链接状态(glLinkProgram),查找编译错误,在调试模式下还可验证当前ES状态是否可执行program中的程序(glValidateProgram),即找出其中运行时错误,根据校验情况,输出错误信息。glValidateProgram操作消耗资源较多,Release模式下通常不调用此函数。示例如下。
// 1、检查链接状态
glLinkProgram(_program);
GLint linkStatus = GL_FALSE;
glGetProgramiv(_program, GL_LINK_STATUS, &linkStatus);if (linkStatus == GL_FALSE) {
GLint logLength = 0;
glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) {
GLchar *logBuffer = calloc(1, logLength);
glGetProgramInfoLog(_program, logLength, NULL, logBuffer); printf("%s", logBuffer);
free(logBuffer);
}
}
// 2、验证Shader是否可执行
glValidateProgram(_program);
glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &logLength);if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(_program, logLength, &logLength, log);
NSLog(@"Program validate log:\n%s", log);
free(log);
}
glGetProgramiv(_program, GL_VALIDATE_STATUS, &status);if (status == 0) { return NO;
}return YES;
6、配置GPU的输入数据缓冲区。这里需要注意,若直接上传数据,则不能映射上传缓冲区到主存去查看上传的数据。用缓冲区(Vertex Buffer Object)则正常。
使用VBO前,可以配置VAO,不配置也不影响运行结果。
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
上传数据方式A、直接上传数据至GPU的实现,yourData为数组。
GLfloat yourData[] = {2, 3, 4, 5, 6};
glEnableVertexAttribArray(0); // layout(location = 0)指定了索引glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, yourData);
对于动态分配的内存,正常绘制三角形没问题,但是,在此却得不到正确的运行结果。
GLfloat *data;
data = malloc(sizeof(GLfloat) * 5);
for (int i = 0; i
data[i] = i + 2;
}
glEnableVertexAttribArray(0); // layout(location = 0)指定了索引
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, yourData);
// 配合glDrawArrays(GL_POINTS, 0, 5);指定了绘制元素数量,解决了动态分配内存无长度信息。
// 然而,Transform Feedback情况无效。
上传数据方式B、用缓冲区(Vertex Buffer Object)。
GLfloat data[] = { 2, 3, 4, 5, 6 };
glGenBuffers(1, &_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, NULL);
尽管PC的多数OpenGL驱动可智能分析glBufferData的内存使用并使用恰当的管理方式,但是,WWDC一个演讲中,苹果的OpenGL ES驱动开发工程师建议我们按数据的使用方式,传递适合的内存管理参数提示值给系统。因此,在此场合,数据只作一次计算,故传递GL_STATIC_DRAW。
glEnableVertexAttribArray(0);指定的索引若在shader中没编写,可通过GLint inputAttribIndex = glGetAttribLocation(program, "inValue");方式获取。
7、配置GPU的输出数据缓冲区并与transform feedback绑定
glGenBuffers(1, &_gpuOutputBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _gpuOutputBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), NULL, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, _gpuOutputBuffer);
glBindBufferBase完成Transform Feedback输出与数据缓冲区的实际绑定。
参数GL_TRANSFORM_FEEDBACK_BUFFER:指定使用Transform Feedback缓冲区。
参数0:表示使用第1个输出属性,本例Vertex Shader虽然没用layout(location = 0)显式修饰,但是它基于0增长,所以,第一个用0表示。
参数_gpuOutputBuffer:指定了用于绑定的VBO,即GPU往指定的位置上写计算结果数据。
8、禁用光栅化等渲染管线后续操作
因为不绘图,光栅化、片段着色器、深度测试等渲染管线后续操作是多余的,故禁用,节省资源。如此一次,所有统一计算单元在本应用的OpenGL ES命令列队中,都异步执行顶点着色器中的代码。值得一提的是,现代GPU已不再区分顶点处理单元和片段处理单元,它们统称统一处理单元(Uniform Process Unit),即,同一个处理单元,会先处理顶点着色器的代码,再执行片段着色器的代码。
glUseProgram(_program);
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(_program);在链接后立即调用,之后再用Buffer给GPU准备数据,也可以的,在绘图调用前上传数据即可,顺序不影响执行结果。
9、进入transform feedback模式glBeginTransformFeedback(GL_POINTS);,虽然指定为点方式,实际并不看到这些点。另外,虽然只是一个GLfloat类型,而空间的点坐标需要三个分量,我们还是把它当成点看。另外,应根据业务需求使用正确的绘制模式,并与glDrawArrays保持一致。
10、绘图调用,glDrawArrays(GL_POINTS, 0, 5);绘图方式与进入变换反馈的模式一样。
11、结束transform feedback模式,glEndTransformFeedback();。
12、同步GPU
因GPU为异步执行,那么映射内存前,需确保前面的ES指令都执行完。有三种方式同步:
glFlush() 刷新ES命令队列,在有限时间内强制执行GPU指令队列。
glFinish()阻塞当前线程并等待所有GPU指令执行完毕。
glWaitSync()需配合同步对象,编程略为麻烦,后续文档再介绍,在此不详述。
简单起见,这里使用glFinish()。
13、映射GPU内存为读取GPU处理结果作准备
现在需要映射GPU Transform Feedback缓冲区空间到CPU地址空间。桌面版GL操作起来非常方便:
GLfloat feedback[5];
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
ES没glGetBufferSubData,操作要曲折些。在ES,可使用glMapBufferRange映射GPU内存。
float *gpuMemoryBuffer = glMapBufferRange(GL_ARRAY_BUFFER, 0, sizeof(data), GL_MAP_READ_BIT);
14、读取GPU计算结果
if (!gpuMemoryBuffer) {
printf(@"gpuMemoryBuffer == null");
}
for (int i = 0; i
printf("gpuMemoryBuffer[%d] = %f\\\\\\\\t", i, gpuMemoryBuffer[i]);
}
printf("\\\\\\\\n");
以数据源'GLfloat data[] = {2, 3, 4, 5, 6};'为例,在Vertex Shader对它作开方运算,打印的值如下:
gpuMemoryBuffer[0] = 1.414214
gpuMemoryBuffer[1] = 1.732051
gpuMemoryBuffer[2] = 2.000000
gpuMemoryBuffer[3] = 2.236068
gpuMemoryBuffer[4] = 2.449490
15、解除映射,glUnmapBuffer(GL_ARRAY_BUFFER);。
需要说明的是,第7步使用glBindBuffer(GL_ARRAY_BUFFER, _gpuOutputBuffer);将GPU输出缓冲区绑定为GL_ARRAY_BUFFER类型,所以后续的BufferData、MapBufferRange和UnmapBufferRange都使用同一参数。然而,对于Transform Feedback,使用GL_TRANSFORM_FEEDBACK_BUFFER也可读出数据,只要保持当前绑定的缓冲区一致。
3、常见问题
3.1、GLKit不使用GLView导致Transform Feedback操作无效
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!self.context) {
NSLog(@"This application requires OpenGL ES 3.0");
abort();
}
// GLKView *view = (GLKView *)self.view;
// view.context = self.context;
// view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[EAGLContext setCurrentContext:self.context];
如果只作计算,一般认为不需要被注释的内容,毕竟不做显示。然而,经测试发现也导致读不到Transform Feedback回来的结果,program、shader等都正常工作。
3.2、Transform Feedback结果缓冲区结果为0
使用glMapBufferRange返回的buffer不为NULL,读取时却是0.0f。可使用glMapBufferRange映射GPU写缓冲区,看看数据是否正常上传,如下代码所示,只打印,不改变上传的数据。
float *gpuInputMemoryBuffer = glMapBufferRange(GL_ARRAY_BUFFER, 0, sizeof(data), GL_MAP_WRITE_BIT);if (! gpuInputMemoryBuffer) { printf("gpuInputMemoryBuffer == null");
}for (int i = 0; i
}printf("\\\\\\\\n");
glUnmapBuffer(GL_ARRAY_BUFFER);
直接上传数据不可使用此方式打印数据,直接上传数据示例代码如下。
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, gpuInputDataArray);
另一种情况是,使用BufferData、glVertexAttribPointer又指定数据源,如:
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STREAM_DRAW);// 上传数据时指定数据源参数glVertexAttribPointer((GLuint) inValuePos, 1, GL_FLOAT, GL_FALSE, 0, data);
不建议这么操作。
简单数学计算无法体现GPU的优势,通常图像处理等场合会有较为明显的GPU处理速度比CPU快的现象。
推荐阅读
▼
最近发现一个好玩的测试,互联网从业者都应该来试下,我得了 85 分,看看你能不能超过我!长按图片识别二维码或者点击阅读原文就可以参与。
微信号:CocoaChinabbs
▲长按二维码“识别”关注即可免费学习 iOS 开发
月薪十万、出任CEO、赢娶白富美、走上人生巅峰不是梦
--------------------------------------
商务合作QQ:2408167315
投稿邮箱:[email protected]