You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从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);
}

额外优化建议

  1. 避免频繁创建纹理:如果视频分辨率固定,创建一次纹理后重复使用,只更新数据即可,减少GPU开销。
  2. 考虑使用IMF2DBuffer:如果你的IMFMediaBuffer支持IMF2DBuffer接口,可以直接用Lock2D获取平面数据,比ConvertToContiguousBuffer更高效(不需要复制数据)。
  3. 硬件加速的格式转换:如果你的GPU支持,可以考虑用ID3D11VideoContext::VideoProcessorBlt把NV12纹理直接转换为RGB纹理,性能比CPU着色器转换更好。

内容的提问来源于stack exchange,提问作者Gary G.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:29:39