1. NV12格式介绍
YUV 4:2:0是视频和图片编码和解码最常用的输入和输出格式。而在D3D中NV12是支持最广泛的YUV 4:2:0格式,主要因为它在GPU中处理的效率最高,只有两个plane, 而且UVplane和Luminance(Y) plane的pitch(stride)相等,高度为前者一半,所以在很多场景都是可以一起处理,而不需要分为两个或者三个plane来分别进行操作,不管是渲染,拷贝,还是在graphic pipeline中通过映射成其他格式,都非常方便和高效。
反观其他的YUV格式,I420/IYUV, YV12/等,虽然在编码的时候是首选格式,但是在D3D中不支持,或者有很多限制,比如不能映射到CPU中进行操作。
2. NV12在GPU中的布局
GPU为了处理数据的高效率,往往每次操作的内存可能需要对齐,比如2KB/16KB/32KB或者64KB等,这样数据传输和拷贝的速度更快,所以实际NV12格式的图像在显存中存放的布局和实际的大小可能不一样,比如一个HD视频解出来一帧图像,分辨率为1920x1080, Y和UV plane每行1920个byte,Y plane高为1080,而UV plane高为540,但是在不同的GPU中,实际存放布局可能完全不同,比如在AMD的GPU中,Y/UV plane的宽一般存为2048 byte(2KB),而高为1088/544 byte,这样做每行数据操作都可以做到2K对齐,高度为16的倍数,整块内存可以做到32KB对齐,如下图所示:
3. 得到GPU中图像数据的pitch/stride和高度
准确得到这些信息是必须的,否则无法准确将GPU的数据拷贝到CPU可以访问的区域,这里列出了如下两种处理方式:
3.1 Decoder输出的IMFSample
很多场景需要从Decoder的输出拷贝到CPU端可访问的memory中,用作编码输入、图像后期处理、合成和调试等。先看如下代码。
IMFSample* pSample;
// Get pSample from the video decoder
ComPtr<IMFMediaBuffer> spMediaBuffer;
ComPtr<IMFDXGIBuffer> spDXGIBuffer;
BYTE *pNV12 = NULL;
D3D11_TEXTURE2D_DESC desc;
ComPtr<ID3D11Texture2D> spTexture;
ComPtr<IMF2DBuffer> sp2DBuffer;
CHECK_HR(hr = pSample->GetBufferByIndex(i, &spMediaBuffer));
CHECK_HR(hr = spMediaBuffer.As(&spDXGIBuffer));
CHECK_HR(hr = spDXGIBuffer->GetResource(IID_PPV_ARGS(&spTexture)));
spTexture->GetDesc(&desc);
CHECK_HR(hr = spMediaBuffer.As(&sp2DBuffer));
if (SUCCEEDED(hr = sp2DBuffer->Lock2D(&pNV12, &pitch)))
{
if (desc.Format == DXGI_FORMAT_NV12 && pPic2DBuf->datafmt_fcc == 'NV12')
{
// Copy Y plane
MFCopyImage(pPic2DBuf->plane[0], width, pNV12, pitch, desc.Width, height);
// Copy U/V plane
MFCopyImage(pPic2DBuf->plane[1], width, pNV12 + (ptrdiff_t)pitch * desc.Height, pitch, desc.Width, height / 2);
}
else
{
printf("Only support NV12 pixel format {current format: %d}.\n", desc.Format);
}
sp2DBuffer->Unlock2D();
}
从上面的显示代码来看,可以通过IMF2DBuffer::Lock2D得到pitch/stride,然后通过texture desc可以得到GPU中实际高度。
3.2 D3D Texture
GPU会对各类输入的图像数据进行处理,比如录屏输入,解码输出等等,利用GPU强大的处理能力,最后会生成一些特效,这时候再用来编码,然后通过各类网络直播协议发送出去,这时候需要直接访问D3D Texture,拿到pitch和高度信息,准确地将后期处理的数据提取出来:
这部分通过一个例子,演示如何把一个NV12 texture的数据存放到指定文件中。
void SaveNV12(ID3D11Texture2D* pTexture2D, const char* nv12_filename, uint32_t image_width, uint32_t image_height)
{
HRESULT hr = S_OK;
D3D11_TEXTURE2D_DESC desc;
ComPtr<ID3D11Texture2D> spTemp;
ComPtr<IDXGISurface> spSurface;
FILE *pFile = NULL;
pTexture2D->GetDesc(&desc);
//desc.Format = DXGI_FORMAT_NV12;
desc.ArraySize = 1;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.Usage = D3D11_USAGE_STAGING;
CHECK_HR(hr = m_pD3D11Device->CreateTexture2D(&desc, NULL, &spTemp));
m_pD3D11DeviceContext->CopyResource(spTemp.Get(), pTexture2D);
CHECK_HR(hr = spTemp.As(&spSurface));
fopen_s(&pFile, nv12_filename, "wb");
if (pFile != NULL) {
DXGI_MAPPED_RECT map;
if (SUCCEEDED(spSurface->Map(&map, DXGI_MAP_READ)))
{
uint32_t h = image_height;
if (h > desc.Height)
h = desc.Height;
uint32_t w = image_width;
if (w > (uint32_t)map.Pitch)
w = map.Pitch;
BYTE* pNV12 = map.pBits;
if (desc.Format == DXGI_FORMAT_NV12 || desc.Format == DXGI_FORMAT_R8_UNORM) {
// Copy Y plane
for (uint32_t lineidx = 0; lineidx < h; lineidx++)
fwrite(pNV12 + (size_t)map.Pitch * lineidx, 1, w, pFile);
}
if (desc.Format == DXGI_FORMAT_NV12)
{
// Copy UV plane
pNV12 += (size_t)map.Pitch*desc.Height;
for (uint32_t lineidx = 0; lineidx < h / 2; lineidx++)
fwrite(pNV12 + (size_t)map.Pitch * lineidx, 1, w, pFile);
}
spSurface->Unmap();
}
fclose(pFile);
}
done:
return;
}
这段代码创建一个CPU可以访问的临时texture(spTemp),把NV12 texture拷贝到这个临时texture,然后通过Texture的IDXGISurface实现,调用IDXGISurface::Map可以得到显存中图像的pitch/stride,同样高度可以通过texture desc拿到。
4. 为图像数据拷贝定制的函数MFCopyImage
Windows SDK提供了函数MFCopyImage用来替代memcpy处理这种带pitch/stride的图像数据,能提高数倍的拷贝效率,因为视频解码或者实时直播场景中,这个操作数据量大且调用频繁,这个函数,能极大提升应用程序的性能,满足实时处理的需求。
HRESULT MFCopyImage(
[in] BYTE *pDest,
[in] LONG lDestStride,
[in] const BYTE *pSrc,
[in] LONG lSrcStride,
[in] DWORD dwWidthInBytes,
[in] DWORD dwLines
);
一般每个plane调用一次,把GPU中的图像数据拷贝到指定的内存中,可以参考3.1中提供的代码。
// Copy Y plane
MFCopyImage(pPic2DBuf->plane[0], width, pNV12, pitch, desc.Width, height);
// Copy U/V plane
MFCopyImage(pPic2DBuf->plane[1], width, pNV12 + (ptrdiff_t)pitch * desc.Height, pitch, desc.Width, height / 2);
如果图像实际Y plane高度和显存中的Y plane数据高度一致,对于NV12来说,只需调用一次,这个时候对应的高度则是
d
w
L
i
n
e
s
=
Y
P
l
a
n
e
h
e
i
g
h
t
∗
3
/
2
dwLines = YPlane height*3 /2
dwLines=YPlaneheight∗3/2
这时候拷贝速度会更快,这种场景下NV12的性能优势就更能体现出来。
这里画了一张图,可以更好理解这个函数:
5. 结论
通过对D3D硬件解码NV12格式做了一些深入的介绍,然后点出了常见的实现陷阱,以及一些通用的GPU到CPU拷贝操作的性能优化,为后面D3D硬件解码,以及利用硬件解码的输出用于高速实时编码准备一些必要前提知识。
原创不易,如果对你有帮助,望点赞和关注。