专栏名称: Cocoa开发者社区
CocoaChina苹果开发中文社区官方微信,提供教程资源、app推广营销、招聘、外包及培训信息、各类沙龙交流活动以及更多开发者服务。
目录
相关文章推荐
iOS中文站  ·  iPhone ... ·  16 小时前  
iOS中文站  ·  iPhone ... ·  16 小时前  
51好读  ›  专栏  ›  Cocoa开发者社区

iOS GPGPU 编程:GPU进行浮点计算并读取结果

Cocoa开发者社区  · 公众号  · ios  · 2016-08-31 08:05

正文

▲点击上方“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)桥接的配置。


  1. 配置EGL上下文

  2. 提供用于计算的顶点着色器和片元着色器

  3. 配置program

  4. 在链接program前配置transform feedback输出属性名

  5. 链接program

  6. 配置GPU的输入数据缓冲区并上传数据至GPU

  7. 配置GPU的输出数据缓冲区并与transform feedback绑定

  8. 禁用光栅化等渲染管线后续操作

  9. 进入transform feedback模式

  10. 绘图调用

  11. 结束transform feedback模式

  12. 同步GPU

  13. 映射GPU内存

  14. 读取GPU计算结果

  15. 解除映射


操作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将输出的属性名。


  • GL_INTERLEAVED_ATTRIBS指定输出属性数值交错写入一个缓冲区。交错数据需要指定读写跨距(stride)。

  • GL_SEPARATE_ATTRIBS为输出属性指定多个目标缓冲区,一对一写入或不同偏移写入到一个缓冲区。


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指令都执行完。有三种方式同步:


  1. glFlush() 刷新ES命令队列,在有限时间内强制执行GPU指令队列。

  2. glFinish()阻塞当前线程并等待所有GPU指令执行完毕。

  3. 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快的现象。


推荐阅读


END





最近发现一个好玩的测试,互联网从业者都应该来试下,我得了 85 分,看看你能不能超过我!长按图片识别二维码或者点击阅读原文就可以参与。



微信号:CocoaChinabbs


▲长按二维码“识别”关注即可免费学习 iOS 开发

月薪十万、出任CEO、赢娶白富美、走上人生巅峰不是梦

--------------------------------------

商务合作QQ:2408167315

投稿邮箱:[email protected]