Mac上OpenGL中的多线程视频渲染显示出严重的闪烁问题

时间:2012-05-21 07:13:09

标签: multithreading performance opengl flicker

我有一个视频播放器应用程序,并使用多个线程,以保持用户交互仍然顺利。

解码视频的线程最初只是将结果帧作为BGRA写入RAM缓冲区,后者通过glTexSubImage2D上传到VRAM,这对于普通视频来说效果很好,但是 - 预计 - 对于HD来说很慢(特别是1920x1080) 。

为了改进这一点,我实现了一种不同类型的池类,它具有自己的GL上下文(我在Mac上的NSOpenGLContext),它与主上下文共享资源。 此外,我更改了代码,以便它使用

glTextureRangeAPPLE( GL_TEXTURE_RECTANGLE_ARB, m_mappedMemSize, m_mappedMem );

glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_STORAGE_HINT_APPLE, GL_STORAGE_SHARED_APPLE);

用于我使用的纹理,以提高上传到VRAM的性能。 我没有上传BGRA纹理(对于1920x1080,每帧大约8MB),我为Y,U和V上传了三个单独的纹理(每个纹理为GL_LUMINANCE,GL_UNSIGNED_BYTE,原始大小为Y纹理,U和V为一半)尺寸),从而减少上传到大约3 MB的大小,这已经显示出一些改进。

我创建了一个YUV纹理池(取决于视频的大小,它通常介于3到8个表面之间(因为它是Y,U和V组件,所以是三个) - 每个纹理映射到它自己的区域上面的m_mappedMem。

当我收到一个新解码的视频帧时,我找到一组免费的YUV曲面,并使用以下代码更新这三个组件:

glActiveTexture(m_textureUnits[texUnit]);
glEnable(GL_TEXTURE_RECTANGLE_ARB);

glBindTexture(GL_TEXTURE_RECTANGLE_ARB, planeInfo->m_texHandle);

glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_STORAGE_HINT_APPLE, GL_STORAGE_SHARED_APPLE);
glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);

memcpy( planeInfo->m_buffer, srcData, planeInfo->m_planeSize );

glTexSubImage2D( GL_TEXTURE_RECTANGLE_ARB, 
                0, 
                0, 
                0, 
                planeInfo->m_width, 
                planeInfo->m_height, 
                GL_LUMINANCE, 
                GL_UNSIGNED_BYTE, 
                planeInfo->m_buffer );

(作为一个附带问题:我不确定每个纹理是否应该使用不同的纹理单元?[我使用单位0表示Y,1表示U,2表示V btw])

一旦完成,我将我使用的纹理标记为正在使用,并且VideoFrame类填充了它们的信息(即纹理编号,以及它们占用的缓冲区中的哪个区域等)并放入队列中被渲染。达到最小队列大小后,将通知主应用程序它可以开始呈现视频。

同时主渲染线程(在确保正确的状态之后等)然后访问该队列(该队列类的内部访问权限受到互斥锁的保护)并呈现顶部帧。

主渲染线程有两个帧缓冲区,并通过glFramebufferTexture2D两个纹理关联它们,以实现某种双缓冲。 在主渲染循环中,它然后检查哪一个是前缓冲区,然后使用纹理单元0将此前缓冲区渲染到屏幕:

glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_RECTANGLE_ARB);            
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, frontTexHandle);            
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT );
glEnableClientState( GL_VERTEX_ARRAY );
glEnableClientState( GL_TEXTURE_COORD_ARRAY );            
glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);
glVertexPointer(4, GL_FLOAT, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, m_texCoordBuffer);
glTexCoordPointer(2, GL_FLOAT, 0, 0);
glDrawArrays(GL_QUADS, 0, 4);
glPopClientAttrib();

在对当前帧的屏幕进行渲染之前(由于视频的通常帧速率约为24 fps,因此在下一个视频帧渲染之前,此帧可能会呈现几次 - 这就是我使用此帧的原因方法)我调用视频解码器类来检查新帧是否可用(即它负责同步到时间轴并用新帧更新后备缓冲区),如果帧可用,那么我将渲染到后台缓冲区纹理从videodecoder类内部(这发生在与主渲染线程相同的线程上):

glBindFramebuffer(GL_FRAMEBUFFER, backbufferFBOHandle);

glPushAttrib(GL_VIEWPORT_BIT);    // need to set viewport all the time?
glViewport(0,0,m_surfaceWidth,m_surfaceHeight);

glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_TEXTURE);
glPushMatrix();
glLoadIdentity();
glScalef( (GLfloat)m_surfaceWidth, (GLfloat)m_surfaceHeight, 1.0f );

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texID_Y);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texID_U);

glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texID_V);

glUseProgram(m_yuv2rgbShader->GetProgram());

glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);
glEnableVertexAttribArray(m_attributePos);
glVertexAttribPointer(m_attributePos, 4, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, m_texCoordBuffer);
glEnableVertexAttribArray(m_attributeTexCoord);
glVertexAttribPointer(m_attributeTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(GL_QUADS, 0, 4);

glUseProgram(0);

glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);                

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);

glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();                            

glPopAttrib();
glBindFramebuffer(GL_FRAMEBUFFER, 0);

[请注意,为简洁起见,我省略了某些安全检查和评论]

在上述调用之后,视频解码器设置一个缓冲区可以交换的标志,并且在从上面进行主线程渲染循环之后,它会检查该标志并相应地设置frontBuffer / backBuffer。使用过的表面也被标记为空闲且可再次使用。

在我的原始代码中,当我使用BGRA并通过glTexSubImage2D和glBegin和glEnd上传时,我没有遇到任何问题,但是一旦我开始改进,使用着色器将YUV组件转换为BGRA,以及那些DMA传输,glDrawArrays开始出现这些问题。

基本上它似乎有点像撕裂效果(顺便说一下,我将GL交换间隔设置为1以与刷新同步),部分就像它在两者之间跳回几帧。

我期望有一个我渲染到的表面池,并且在渲染到目标表面后释放它们,并且对目标表面进行双缓冲应该足够了,但显然需要在其他地方完成更多的同步 - 但是,我真的不知道如何解决这个问题。

我假设因为glTexSubImage2D现在由DMA处理(并且函数根据应该立即返回的文档)上传可能尚未完成(并且下一帧正在渲染),或者我忘了(或者不知道)关于OpenGL(Mac)所需的其他同步机制。

在我开始优化代码之前,根据OpenGL分析器:

  • glTexSubImage2D中几乎70%的GLTime(即将8MB BGRA上传到VRAM)
  • CGLFlushDrawable中的近30%

在我将代码更改为上面后,它现在说:

  • glTexSubImage2D中约4%GLTime(因此DMA似乎运行良好)
  • GLCFlushDrawable中的16%
  • glDrawArrays中差不多75%(对我来说是个大惊喜)

对这些结果有何评论?

如果您需要有关我的代码设置方式的更多信息,请告诉我们。关于如何解决这个问题的提示将非常感激。

编辑:这是我的着色器供参考

#version 110
attribute vec2 texCoord;
attribute vec4 position;

// the tex coords for the fragment shader
varying vec2 texCoordY;
varying vec2 texCoordUV;

//the shader entry point is the main method
void main()
{   
    texCoordY = texCoord ;
    texCoordUV = texCoordY * 0.5;
    gl_Position = gl_ModelViewProjectionMatrix * position;
}

和片段:

#version 110

uniform sampler2DRect texY;
uniform sampler2DRect texU;
uniform sampler2DRect texV;

// the incoming tex coord for this vertex
varying vec2 texCoordY;
varying vec2 texCoordUV;

// RGB coefficients
const vec3 R_cf = vec3(1.164383,  0.000000,  1.596027);
const vec3 G_cf = vec3(1.164383, -0.391762, -0.812968);
const vec3 B_cf = vec3(1.164383,  2.017232,  0.000000);

// YUV offset
const vec3 offset = vec3(-0.0625, -0.5, -0.5);

void main()
{
    // get the YUV values
    vec3 yuv;
    yuv.x = texture2DRect(texY, texCoordY).r;
    yuv.y = texture2DRect(texU, texCoordUV).r;
    yuv.z = texture2DRect(texV, texCoordUV).r;
    yuv += offset;

    // set up the rgb result
    vec3 rgb;

    // YUV to RGB transform
    rgb.r = dot(yuv, R_cf);
    rgb.g = dot(yuv, G_cf);
    rgb.b = dot(yuv, B_cf);

    gl_FragColor = vec4(rgb, 1.0);
}

编辑2:作为旁注,我有另一个使用VDADecoder对象进行解码的渲染管道,它在性能上非常好地工作,但具有相同的闪烁问题。所以我的代码中的线程肯定存在一些问题 - 到目前为止我还无法弄清楚究竟是什么。但我还需要为那些不支持VDA的机器提供软件解码器解决方案,因此CPU负载非常高,因此我尝试将YUV转换为RGB转换为GPU

2 个答案:

答案 0 :(得分:1)

从我看到的内容(即glPushMatrix调用等)我假设您使用的不是最新的硬件,很可能您遇到了旧版视频卡的问题,例如CGLFlushDrawable Why is CGLFlushDrawable so slow? (I am using VBOs)

你说的第二件事是YUV-> RGB着色器,它显然多次访问源纹理,并且必须在任何显卡上都很慢,尤其是较旧的显卡。因此,glDrawArrays()调用的大时间实际上反映了这样一个事实:即使着色器代码看起来“无辜”,你也会使用非常繁重的着色器程序(就内存访问而言)。

着色器代码访问纹理(以及系统的RAM),就性能而言(对于此视频卡)与执行RAM-> VRAM副本相同。

一般建议:尝试避免使用非矩形和非幂二纹理。这也会破坏性能。还应避免使用任何非标准纹理格式和扩展。越简单越好。尝试使用像2048x1024纹理或2048x2048这样的东西,如果你真的需要FullHD分辨率(顺便说一句,这个应该在纯粹的算术中慢)。

答案 1 :(得分:1)

好的,经过大量的测试和研究,我终于设法解决了我的问题:

首先我尝试使用帧缓冲区(使用glFramebufferTexture2D作为颜色附件0绑定到该纹理)来写入目标纹理,然后在同一帧中尝试从框架渲染到窗口时读取它帧缓存。

基本上我错误地认为(在相同的帧中被调用,并且直接在彼此中连续)第一次调用将完成写入帧缓冲,然后下一个调用将从中读取。因此,调用glFlush(对于使用VDADecoder的类)和glFinish(对于使用软件解码器的类)都可以实现。

在旁注中:如上面的评论中所示,我更改了我的整个代码,因此它不再使用固定管道,并使其看起来更干净。 OpenGL Profiler下的性能测试(在Mac OS X 10.7下)表明,从原始代码到当前代码的更改已将OpenGL使用的总时间从近50%减少到约15%(释放更多资源)对于实际的视频解码 - 在VDADecoder对象不可用的情况下。)