在Mac上使用OpenGL进行多线程视频渲染时出现严重闪烁问题

3
我有一个视频播放器应用程序,为了保持用户交互的流畅性,使用了多个线程。解码视频的线程最初将生成的帧作为BGRA写入RAM缓冲区,然后通过glTexSubImage2D上传到VRAM,这对于普通视频来说已经足够好了,但是对于高清视频(特别是1920x1080)会变得很慢。 为了改进这一点,我实现了一种不同类型的池类,它具有自己的GL上下文(因为我在Mac上),并与主上下文共享资源。此外,我更改了代码,使其使用
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]) 完成后,我所使用的纹理被标记为已使用,并且填充了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,因此在下一帧视频被渲染之前,该帧可能会被渲染多次 - 这就是我使用这种方法的原因),我调用视频解码器类来检查是否有新的帧可用(即它负责同步时间轴并使用新帧更新后备缓冲区),如果有可用帧,则从视频解码器类内部将其渲染到后备缓冲纹理中(这发生在与主渲染线程相同的线程上)。
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);
请注意,为了简洁起见,我省略了某些安全检查和注释。 在上述调用之后,视频解码器设置一个标志,表示缓冲区可以交换,在上面的主线程渲染循环之后,它检查该标志并相应地设置前缓冲区/后缓冲区。此外,使用的表面被标记为自由和可用状态。 在我的原始代码中,当我使用BGRA和通过glTexSubImage2D和glBegin和glEnd上传时,我没有遇到任何问题,但一旦我开始改进事情,使用着色器将YUV组件转换为BGRA,并进行DMA传输和glDrawArrays,这些问题开始出现。 基本上,它似乎部分像是撕裂效果(顺便说一下,我将GL交换间隔设置为1以与刷新同步),部分地像是在其中跳回几帧。 我期望拥有一个池,我可以将其渲染到,并且在渲染到目标表面后将其释放,并且双缓冲该目标表面应该足够了,但显然还需要在其他地方进行更多的同步处理-但是我真的不知道如何解决这个问题。 我假设因为现在通过DMA处理glTexSubImage2D(根据文档,该函数应立即返回),因此上传可能尚未完成(下一帧正在其上渲染),或者我忘记了(或不知道)需要为OpenGL(Mac)进行其他同步机制。 根据OpenGL分析器在我开始优化代码之前的情况: - 几乎70%的GLTime在glTexSubImage2D中(即将8MB BGRA上传到VRAM) - 几乎30%在CGLFlushDrawable中 而在我将代码更改为上述内容之后,它现在显示: - 约4%的GLTime在glTexSubImage2D中(因此DMA似乎运行良好) - 16%在GLCFlushDrawable中 - 几乎75%在glDrawArrays中(这让我大吃一惊) 对于这些结果有什么评论吗? 如果您需要有关我的代码设置的任何其他信息,请告诉我。非常感谢您提供解决此问题的提示。 编辑:这里是我的着色器供参考。
#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上。


你是否使用带有回调函数的CVDisplayLink来渲染你的帧? - Viktor Latypov
@Viktor:不,我没有使用CVDisplayLink,我有一个主渲染循环,它渲染UI(一个不同的子系统),并以最大60 fps进行渲染,作为其中的一部分是调用mediaplayer的更新(上面的代码将呈现到当前的“前缓冲表面”)。 - Bjoern
1
不确定问题出在哪里,但你做了很多好事情:YUV上传,着色器中的YUV2RGB转换,纹理池以便DMA发生。但你可能不应该使用自定义渲染目标。解码60fps的YUV应该更快且更少出错。 - Calvin1602
哦,我明白你的意思了。 我唯一的担心是,如果我的运行速度是60 fps(通常情况下似乎是这样),那么一个视频帧(因为视频通常约为24 fps)将被渲染多次,因此我想避免多次进行YUV到RGB的转换...我会研究一下的,谢谢你的回复。 - Bjoern
@Calvin1602 - 如果你感兴趣的话:经过我最近的一些更改(即远离固定管线,使用DMA,上传YUV而不是BGRA,清理冗余调用,不使用客户端矩阵等),OpenGL分析器显示1920x1080高清电影中OpenGL应用程序总时间在OpenGL中所占比例从几乎50%降至现在仅约15%-所以我对此感到非常满意 :-) 现在只需要解决剩下的闪烁问题(已经有所改善)... - Bjoern
显示剩余3条评论
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
1

从我所看到的(即glPushMatrix调用等),我认为您正在使用不是最新的硬件,很可能会遇到旧视频卡的问题,比如CGLFlushDrawable Why is CGLFlushDrawable so slow? (I am using VBOs)

您说的第二件事是YUV->RGB着色器,显然会多次访问源纹理,这在任何视频卡上都会很慢,特别是旧的视频卡。因此,glDrawArrays()调用的大时间实际上反映了您正在使用非常重的着色器程序(在内存访问方面),即使着色器代码可能看起来“无害”。

着色器代码访问纹理(因此访问系统RAM),在性能方面(对于这个视频卡)与执行RAM->VRAM复制相同。

一般建议:尽量避免使用非矩形和非2的幂次方纹理,这也会影响性能。任何非标准的纹理格式和扩展都应该被避免。越简单越好。如果你真的需要FullHD分辨率,可以尝试使用2048x1024或2048x2048的纹理(顺便说一下,这应该会因为纯数学计算而变慢)。

感谢您的回复。实际上,我对OpenGL渲染还很新,因此我的代码可能看起来不正确或过时,我很高兴能得到任何有关如何改进我的代码和呈现这些四边形与电影帧的提示。关于硬件:为了进行测试,我正在使用一台MacBook Air(最新版本,带有英特尔图形芯片)。是的,我知道YUV2RGB会对GPU造成很大负担,但是我想避免每帧上传8MB而选择3MB,并释放CPU(在我们的应用程序中,CPU正在做很多其他事情)。请查看我的问题以获取着色器代码(刚刚添加)。 - Bjoern
英特尔...很遗憾,它只是一个不太适合图形处理的机器。这就解释了为什么基于 CPU 的 YUV 转换速度更快 - 处理器相对于显卡来说更加出色。需要注意的是:在 OSX Lion 中,OpenGL 3.1+ 核心配置文件只会废弃“固定功能管线”,其中包括 glPushMatrix() 等函数。由于您已经熟悉着色器,我建议您将投影/视口矩阵传递给您的着色器,并避免使用旧的 Push/Pop 函数。这可能会有所帮助。 - Viktor Latypov
关于着色器,我添加了一些想法和建议。这里没有更多要说的了,我猜。 - Viktor Latypov
好的,谢谢你提醒,那么我明天会移除那些推入/弹出矩阵调用,并让代码看起来更现代化。 - Bjoern
将Push/Pop更改为glUniform调用。当然,不要使用符号名称,只使用整数ID。应避免使用PushAttrib。每帧都不需要glViewport。在设置或窗口调整大小时调用一次即可。 - Viktor Latypov
显示剩余5条评论

1

好的,经过更多的测试和研究,我终于成功解决了我的问题:

问题出在我最初尝试使用帧缓冲区(使用glFramebufferTexture2D将其绑定到纹理作为颜色附件0)写入目标纹理,并在同一帧中尝试从中读取时,渲染到窗口帧缓冲区。

基本上,我错误地假设(在同一帧中直接连续调用),第一个调用会在下一个调用读取之前完成对帧缓冲区的写入。因此,对于使用VDADecoder的类,调用glFlush,以及对于使用软件解码器的类,调用glFinish就解决了问题。

顺便说一句:如上面的评论所示,我改变了整个代码,不再使用固定管线,并使其看起来更加清晰。在OpenGL Profiler(在Mac OS X 10.7下)下进行的性能测试表明,从我的原始代码到当前代码的更改已将OpenGL使用的总应用程序时间减少了近50%至约15%(为实际视频解码释放更多资源-如果VDADecoder对象不可用)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,