如何从H.264 NV12格式的IMFSample输出渲染至DirectX11纹理?
问题描述
是否有优质示例展示如何渲染H.264解码器输出的IMFSample?我的场景使用4K分辨率H.264流,但目标PC仅支持DXGI缓冲区处理1080p内容。不过H.264解码器可处理4K,因此我需找到将NV12格式的IMFSample直接传入DirectX 11渲染器的方法。我已尝试使用DX11VideoRenderer示例,但因该IMFSample不具备IMFDXGIBuffer接口而失败。
在DX11VideoRenderer中,输入的IMFDXGIBuffer为NV12类型时可成功通过硬件渲染。那么NV12类型的非DXGI缓冲区理论上也应被支持?
或许我需要创建NV12格式的ID3D11Texture2D纹理或资源?我找到了从文件创建纹理的示例,但未找到从Sample创建纹理的示例,后者显然更实用。另外,若能创建NV12纹理,如何确定D3D11_SUBRESOURCE_DATA结构中SysMemPitch和SysMemSlicePitch的值?
若能提供任何帮助,我将不胜感激!谢谢。
核心思路:将IMFSample的NV12数据复制到D3D11纹理
你遇到的问题本质是非DXGI-backed的IMFSample无法被DX11VideoRenderer直接识别——因为该示例默认只处理带IMFDXGIBuffer接口的样本。但完全可以手动把NV12数据从IMFSample提取出来,上传到D3D11纹理,再交给渲染管线处理。下面是具体步骤和代码示例:
步骤1:从IMFSample中提取NV12数据
首先,你需要从IMFSample里获取IMFMediaBuffer,然后锁定缓冲区拿到原始NV12数据指针:
HRESULT ExtractNV12Data(IMFSample* pSample, BYTE** ppYData, BYTE** ppUVData, UINT* pYPitch, UINT* pUVPitch, UINT width, UINT height) { HRESULT hr = S_OK; IMFMediaBuffer* pBuffer = nullptr; DWORD cbTotalLength = 0; DWORD cbCurrentLength = 0; BYTE* pData = nullptr; // 获取样本中的第一个媒体缓冲区(NV12是平面格式,通常单个缓冲区包含Y和UV平面) hr = pSample->ConvertToContiguousBuffer(&pBuffer); if (FAILED(hr)) goto done; // 获取缓冲区大小 hr = pBuffer->GetTotalLength(&cbTotalLength); if (FAILED(hr)) goto done; // 锁定缓冲区获取数据指针 hr = pBuffer->Lock(&pData, nullptr, &cbCurrentLength); if (FAILED(hr)) goto done; // 计算Y和UV平面的起始指针和步长 *pYPitch = MFComputeImagePitch(MFVideoFormat_NV12, width, 0); *pUVPitch = *pYPitch; // NV12的UV平面步长和Y相同 *ppYData = pData; *ppUVData = pData + (*pYPitch * height); done: SafeRelease(&pBuffer); return hr; }
这里用MFComputeImagePitch计算步长是最可靠的,它会考虑内存对齐要求,比手动计算更稳妥。
步骤2:创建NV12格式的ID3D11Texture2D
接下来,创建一个兼容的D3D11纹理,用于存储NV12数据。注意要设置BindFlags为D3D11_BIND_SHADER_RESOURCE,这样后续可以创建着色器资源视图(SRV)供渲染使用:
HRESULT CreateNV12Texture(ID3D11Device* pDevice, UINT width, UINT height, ID3D11Texture2D** ppTexture) { HRESULT hr = S_OK; D3D11_TEXTURE2D_DESC desc = {}; desc.Width = width; desc.Height = height; desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = DXGI_FORMAT_NV12; desc.SampleDesc.Count = 1; desc.SampleDesc.Quality = 0; desc.Usage = D3D11_USAGE_DEFAULT; // 默认用法,支持GPU读写 desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; desc.CPUAccessFlags = 0; hr = pDevice->CreateTexture2D(&desc, nullptr, ppTexture); return hr; }
如果你的场景需要频繁更新纹理,也可以考虑用D3D11_USAGE_DYNAMIC + D3D11_CPU_ACCESS_WRITE,但DEFAULT用法性能更好,适合用UpdateSubresource上传数据。
步骤3:将NV12数据上传到D3D11纹理
拿到数据指针和纹理后,用ID3D11DeviceContext::UpdateSubresource把CPU侧的NV12数据上传到GPU纹理:
void UploadNV12ToTexture(ID3D11DeviceContext* pContext, ID3D11Texture2D* pTexture, BYTE* pYData, BYTE* pUVData, UINT yPitch, UINT uvPitch, UINT width, UINT height) { D3D11_SUBRESOURCE_DATA subresourceData[2] = {}; // Y平面(子资源0) subresourceData[0].pSysMem = pYData; subresourceData[0].SysMemPitch = yPitch; subresourceData[0].SysMemSlicePitch = 0; // 2D纹理不需要SlicePitch // UV平面(子资源1) subresourceData[1].pSysMem = pUVData; subresourceData[1].SysMemPitch = uvPitch; subresourceData[1].SysMemSlicePitch = 0; // 更新纹理的两个子资源 pContext->UpdateSubresource(pTexture, 0, nullptr, subresourceData[0].pSysMem, yPitch, 0); pContext->UpdateSubresource(pTexture, 1, nullptr, subresourceData[1].pSysMem, uvPitch, 0); }
关于SysMemPitch和SysMemSlicePitch的说明:
SysMemPitch:一行像素的字节数(包含内存对齐的填充),直接用MFComputeImagePitch的结果即可,不需要手动计算。SysMemSlicePitch:仅针对3D纹理或纹理数组,2D纹理的话设为0即可,因为没有深度切片。
步骤4:将纹理传入DX11渲染管线
现在你可以为这个NV12纹理创建一个ID3D11ShaderResourceView,然后在像素着色器中采样NV12数据并转换为RGB格式(因为大多数显示器不支持直接显示NV12)。
示例像素着色器代码(HLSL):
Texture2DArray<float2> NV12Texture : register(t0); SamplerState NV12Sampler : register(s0); float4 PS(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_TARGET { // 采样Y和UV分量 float Y = NV12Texture.Load(int3(uv.x, uv.y, 0)).x; float2 UV = NV12Texture.Load(int3(uv.x / 2, uv.y / 2, 1)).xy - 0.5; // NV12转RGB BT.709标准转换 float R = Y + 1.5748 * UV.y; float G = Y - 0.1873 * UV.x - 0.4681 * UV.y; float B = Y + 1.8556 * UV.x; return float4(R, G, B, 1.0); }
额外优化建议
- 避免频繁创建纹理:如果视频分辨率固定,创建一次纹理后重复使用,只更新数据即可,减少GPU开销。
- 考虑使用
IMF2DBuffer:如果你的IMFMediaBuffer支持IMF2DBuffer接口,可以直接用Lock2D获取平面数据,比ConvertToContiguousBuffer更高效(不需要复制数据)。 - 硬件加速的格式转换:如果你的GPU支持,可以考虑用
ID3D11VideoContext::VideoProcessorBlt把NV12纹理直接转换为RGB纹理,性能比CPU着色器转换更好。
内容的提问来源于stack exchange,提问作者Gary G.

