3

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

解码视频的线程最初只是将生成的帧作为 BGRA 写入 RAM 缓冲区,该缓冲区由 glTexSubImage2D 上传到 VRAM,这对于普通视频来说工作得很好,但是 - 正如预期的那样 - 对于高清(尤其是 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 个表面之间(乘以 3,因为它是 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,因此在渲染下一个视频帧之前,可能会渲染此帧几次 - 这就是我使用这种方法的原因)我调用视频解码器检查新帧是否可用的类(即,它负责同步到时间线并用新帧更新后备缓冲区),如果帧可用,那么我将从视频解码器类内部渲染到后备缓冲区纹理(这个发生在与主渲染线程相同的线程上):

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 似乎运行良好)
  • 16% 在 GLCFlushDrawable
  • 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

4

2 回答 2

1

从我所看到的(即 glPushMatrix 调用等)我假设您使用的不是最新的硬件,并且很可能您遇到了旧显卡的问题,例如 CGLFlushDrawable为什么 CGLFlushDrawable 这么慢?(我正在使用 VBO)

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

着色器代码访问纹理(以及系统的 RAM),它在性能方面(对于此显卡)与执行 RAM->VRAM 复制相同。

一般建议:尽量避免使用非矩形和非二次幂纹理。这也可能会扼杀性能。任何非标准的纹理格式和扩展也应该避免。越简单 - 越好。如果您真的需要 FullHD 分辨率,请尝试使用 2048x1024 纹理或 2048x2048 之类的东西(顺便说一下,这应该很慢纯算术)。

于 2012-05-21T12:46:41.693 回答
1

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

发生的事情是,首先我尝试使用帧缓冲区(使用 glFramebufferTexture2D 作为颜色附件 0 绑定到该纹理)写入目标纹理,然后在将帧渲染到窗口帧缓冲区时尝试在同一帧中读取它。

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

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

于 2012-05-28T08:32:50.057 回答