我有一个视频播放器应用程序,并使用多个线程来保持用户交互仍然流畅。
解码视频的线程最初只是将生成的帧作为 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