如何用DirectX直接渲染显示FFMPEG的dxva2硬件解码的数据

ffmpeg现在封装的很是so easy,使用上不用多讲。
如何启用硬件解码,在ffmpeg源码中(doc\example\hw_decode.c)中也有完整样例。

enum AVHWDeviceType hwDeviceType;
hwDeviceType = av_hwdevice_find_type_by_name("dxva2");
// 尝试硬解码
if (hwDeviceType != AV_HWDEVICE_TYPE_NONE)
{
	decodecCtx->get_format = get_hw_format;
	if (hw_decoder_init(decodecCtx, hwDeviceType) < 0)
        	printf("hw_decoder_init failed.\n");
	else
        	printf("User dxva2 decodec.\n");
}

样例中提供了get_hw_format函数和hw_decoder_init,照抄过来,即可启用硬解码。

与软解码流程一样,给解码器avcodec_send_packet 设置数据后,调用avcodec_receive_frame 即可拿到解码后的AVFrame数据。

dxva2解码数据以IDirect3DSurface9 纹理表面接口提供,保存在(IDirect3DSurface9 *)AVFrame->data[3]中。
IDirect3DSurface9 在显存GPU内部是NV12格式,这里ffmpeg提供了av_hwframe_transfer_data函数,将IDirect3DSurface9 纹理表面的显存数据传输到NV12格式的CPU内存中。

// GPU->CPU->Scale->View
if (av_hwframe_transfer_data(hw_frame, av_frame, 0) >= 0)
{
	av_frame_copy_props(hw_frame, av_frame);
	av_frame_unref(av_frame);
	p_frame = hw_frame;
}

到这里就可以继续沿用软件缩放和显示流程了。都是奔硬解来的,这里又变回软处理。
avcodec_send_packet 在ffmpeg内部是在给显卡的GPU显存送数据,编码数据量并不大,耗时不多。
解码后的原始图像数据就大了,等av_hwframe_transfer_data再从GPU传输回内存中,这里耗费的CPU资源,基本可以比肩软解码的速度。
如果再使用DirectX9接口送回显存去渲染显示,一来一去,速度就比较搞笑了,也失去了硬解的意义。
还是要想办法直接显示IDirect3DSurface9。

但ffmpeg样例并没有提供内部DirectX3D的接口调用,相关的接口资源也没有暴露,如果自己去创建IDirect3DDevice9接口,显然是没有办法绘制ffmpeg内部Direct3D对象创建的IDirect3DSurface9。
直接改ffmpeg源码工作量也并不合算。

还好我们可以拿到(IDirect3DSurface9 *)AVFrame->data[3],IDirect3DSurface9接口提供了GetDevice函数,可以获取到ffmpeg内部的IDirect3DDevice9接口。

CComPtr<IDirect3DDevice9> pD3DDevice;
hr = pSurface->GetDevice(&pD3DDevice);
if (FAILED(hr))
	return FALSE;

用它即可直接绘制位显存中的纹理表面。

这里还没完,ffmpeg内部创建的设备翻转链表的BackBufferWidth/BackBufferHeight尺寸分别只有640/480,窗口句柄是桌面窗口,同样ffmpeg没有提供暴露接口参数。
还好IDirect3DDevice9提供多个窗口绘制能力,这里需要添加自己的窗口绘制翻转链表。
这里的翻转链表,类似双缓冲翻转显示,GDI绘制自定义UI经常会用的技巧。
如果有同学还不清楚DirectX3D的绘制方式,这里建议先百度补习下相关用法。

D3DDISPLAYMODE d3ddm;
hr = m_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
if (FAILED(hr))
	return FALSE;

if (nWidth > d3ddm.Width)
	nWidth = d3ddm.Width;
if (nHeight > d3ddm.Height)
	nHeight = d3ddm.Height;

CComPtr<IDirect3DSwapChain9> spSwapChain;
hr = m_pDevice->GetSwapChain(0, &spSwapChain);
if (FAILED(hr))
	return FALSE;

hr = spSwapChain->GetPresentParameters(&m_Present);
if (FAILED(hr))
	return FALSE;

m_Present.hDeviceWindow = m_hWnd;
m_Present.Windowed = TRUE;
m_Present.BackBufferWidth = nWidth;
m_Present.BackBufferHeight = nHeight;
m_pAddSwapChain.Release();

hr = m_pDevice->CreateAdditionalSwapChain(&m_Present, &m_pAddSwapChain);
if (FAILED(hr))
	return FALSE;

我这里首先获取了显卡设备的桌面分辨率,设置给翻转链表的BackBufferWidth/BackBufferHeight(创建大于桌面分辨率的BackBuffer有意义?),设置窗口句柄,CreateAdditionalSwapChain添加自己的窗口绘制翻转链表。
有了自己的窗口绘制翻转链表,就可以把IDirect3DSurface9渲染到自己的窗口上。
首先需要设置IDirect3DDevice9的Render渲染目标为我们自己添加的窗口绘制翻转量表的BackSurface。

CComPtr<IDirect3DSurface9> spBackSurface;
hr = m_pAddSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &spBackSurface);
if (FAILED(hr))
	return FALSE;

hr = m_pDevice->SetRenderTarget(0, spBackSurface);
if (FAILED(hr))
	return FALSE;

接下来是显示,这里直接将表面StretchRect到翻转链的背面,然后翻转即可显示。
StretchRect时即可在显卡GPU内部完成NV12到BackBuffer的FMT格式转换,以及缩放,都是硬件实现。
因为上面设置了Render渲染目标为自创建的翻转链,当然也可以使用三维的Render渲染方式,
因为不需要翻转,或者旋转之类的特效,我用的StretchRect后翻转显示。

hr = m_pDevice->StretchRect(pSrcSurface, pRectSrc, spBackSurface, pRectDec, D3DTEXF_LINEAR);
if (FAILED(hr))
	return FALSE;
hr = m_pAddSwapChain->Present(pSrcRect, pDecRect, NULL, NULL, 0);
if (FAILED(hr))
	return FALSE;

Render渲染的代码太长就不贴出来了,有兴趣可以看微软的DX文档,使用ID3DXSprite二维精灵绘制接口比较便捷。
至此,我们已经可以完全依赖GPU去解码并显示视频。

这里额外提一句,DX9的文档上关于IDirect3DDevice9::StretchRect有这么一句:
StretchRect cannot be called inside of a BeginScene/EndScene pair.
实际上我看网上很多代码依然是这样:

hr = m_pDevice->BeginScene();
hr = m_pDevice->StretchRect(pSrcSurface, pRectSrc, spBackSurface, pRectDec, D3DTEXF_LINEAR);
hr = m_pDevice->EndScene();

网络上的代码还是别简单的copy来用…
额外提些要点,
能开启dxva2的显卡,应该都支持NV12->RGB的硬件转换,至少我在几个不同的PC,以及平板上,获取设备表面格式转换能力,都是返回OK。但若遇到硬件不支持,还是转为软件处理兼容性最佳。

工作之余笔记,难免有错,欢迎拍砖。
若有帮助幸甚。

posted @ 2021-09-12 23:07  裤子多多  阅读(3100)  评论(3编辑  收藏  举报