调试一例调用D3D11硬解码用于板卡播出,使用智能指针CComPtr不仔细引发的bug,很囧
最近做个播放器,调用播出板卡播出一个视频信号用于测试,硬件那边希望可以循环播放以便能长时间跑测试,于是现改了一个测试版本。程序跑了一晚上,第二天去看,播放器死了。
首先怀疑板卡的播出调用未返回(凭以前做板卡驱动的直观经验),立马调用VS附加到卡死状态的程序上调试(程序有DEBUG编译),暂停程序后一条线程一条线程的排查,果然是在板卡设置数据线程里,设置音频数据未返回(block模式)。
查看板卡SDK文档,发现可以使用nonblock模式,于是暂改用非阻塞来排查板卡播出流程哪里有问题。
然后发现是循环播了多次视频文件以后,视频数据没有设置,因此板卡无法同步设置音频数据。
非阻塞模式下设置音频数据,一直返回设置0字节,阻塞模式下则一直等设置视频数据,会阻塞住不返回。这里板卡驱动的SDK API设计其实有点不妥,应该返回一个异常或者错误,以便应用层处理。
再切换到设置视频相关范围上逐模块排查,最终确认再反复播9次视频文件后,第10次播放视频文件,便不能启用D3D11的H264硬件解码了。
FFMPEG调用的硬解码初始化一切正常,但是avcodec_receive_frame获得的AVFrame指针里format视频格式为默认值0(AV_PIX_FMT_YUV420P枚举),并非AV_PIX_FMT_D3D11硬解码枚举(这里使用FFMPEG做硬件解码的同学们,一定注意检查拿到AVFrame的格式),硬解码上下文虽然创建成功,但是实际出来的是软解码数据,因而没有调用av_hwframe_transfer_data从纹理中download内存数据,自然没有调用板卡视频设置接口。
因为这份调用FFMPEG编解码的代码其实用得比较久了,以前从未遇到这类问题。鉴于工作用机的显卡比较古董,一开始怀疑显卡驱动古旧问题,内部有bug?程序员遇到bug,内心潜在的第一反应就是别人的: )。但更换显卡得动手拆机子,还是先软件上自我排查吧。也不排除FFMPEG的编解码确实有问题。
重新把代码的解码播放部分整理成一个独立的小模块,启动播放然后立即关闭10次,然后再模块内部逐个功能关闭反复测试,最终发现把显示绘制部分关闭,模块反复开启关闭均解码正常。这里可以确认排除编解码模块的问题。转而排查显示部分。
因为使用的硬解码,解码出来的是ID3D11Texture2D的纹理指针,D3D11的显示部分会使用这个纹理去获取ID3D11Device,再重新链接指定绘制窗口的翻转链表,再去显示绘制纹理。
还是用逐句功能关闭测试排除问题,确定初始化显示模块,反复重启即正常。怀疑是显示部分有接口未释放,但D3D11显示部分使用的是ComPtr智能指针,内部不可能会有未释放问题。同样这模块代码也用了很久了,以前并未发现有此问题。并且做为C/C++老手,我一直遵循创建获取对象同时,即写好释放的代码的习惯。
不过淹死的都是会游泳的,我还是写了个测试模块。连续去获取10个D3D11的Com接口,立即报"引发的异常: Microsoft C++ 异常: _com_error...",跟反复重启显示模块所报异常信息一样。就是Com接口指针释放问题。
显示模块内部的CComPtr指针使用和释放肯定不会有问题,只有看外面调用部分了,就只有两句调用。一看,果然在获得FFMPEG解码AVFrame中的ID3D11Texture2D纹理指针后,先使用
CComPtr<ID3D11Device> spD3DDevice;
pTexture2D->GetDevice(&spD3DDevice);
获取ID3D11Device,然后
ID3D11DeviceContext* pD3DDeviceContext;
spD3DDevice->GetImmediateContext(&pD3DDeviceContext);
获取ID3D11DeviceContext设备上下文,这里搞忘使用CComPtr了,使用的普通指针。
普通指针在赋值给显示部分用的ComPtr类型指针时,内部会AddRef一次,原普通指针需要手工释放。赋值时应该用Attach,赋值同时释放掉原有指针。或者改为同样使用CComPtr即可。
CComPtr<ID3D11DeviceContext> spD3DDeviceContext;
spD3DDevice->GetImmediateContext(&spD3DDeviceContext);
老司机也有撞车的时候。 U·ェ·U