D3D处理2D图像: NV12格式及其转换(1)

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对齐,如下图所示:

在这里插入图片描述

图二
在显存中实际一行的字节宽度称为pitch或者stride,在NV12中Y plane和UV plane它是相同的。很多程序实现不严谨,往往造成在一些显卡上播放这些视频造成错误,比如著名的免费开源播放器, VLC播放器在一些AMD显卡上播放1920x1080的视频,上方会有一段绿色区域:

在这里插入图片描述

图三
造成这个原因主要是错误把Y后面的高度padding数据当作UV plane的起始地址,从而导致Chroma部分显示出错。

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=YPlaneheight3/2
这时候拷贝速度会更快,这种场景下NV12的性能优势就更能体现出来。
这里画了一张图,可以更好理解这个函数:
在这里插入图片描述

图四
上图第一个图像是GPU中图像数据布局,以及对应MFCopyImage的输入参数的标识,第二、三个图则是描述拷贝目标地址的参数描述。另外,这个函数还具有图像crop的功能,在某些特殊场景,还能高效简化这部分处理,做到又快又好。

5. 结论

通过对D3D硬件解码NV12格式做了一些深入的介绍,然后点出了常见的实现陷阱,以及一些通用的GPU到CPU拷贝操作的性能优化,为后面D3D硬件解码,以及利用硬件解码的输出用于高速实时编码准备一些必要前提知识。

原创不易,如果对你有帮助,望点赞和关注。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
D3D渲染是指使用Direct3D图形编程接口进行图形渲染的过程。NV12是一种图像颜色格式,其中Y代表亮度信息,U和V代表色度信息。下面是使用D3D渲染NV12图像的步骤简述: 1. 创建D3D设备:首先需要创建一个D3D设备来进行图形渲染。通过调用D3D的相关函数,可以创建一个设备对象,并且配置设备的显示参数,如分辨率、帧率等。 2. 创建纹理和顶点缓冲:在D3D渲染中,需要创建对应NV12格式的纹理和顶点缓冲,用于存储图像数据和顶点数据。可以使用D3D提供的函数来创建纹理和顶点缓冲,并设置其格式和大小。 3. 加载NV12图像数据:将NV12格式图像数据加载到纹理中。可以使用D3D提供的相关函数,将NV12图像数据转换成适用于D3D渲染的格式,并将数据加载到纹理中。 4. 自定义着色器:为了正确渲染NV12图像,需要自定义着色器来对纹理中的像素进行处理。可以使用HLSL语言编写着色器代码,并通过D3D进行编译和加载。 5. 绘制图像:通过调用D3D的绘制函数,将纹理中的数据渲染到屏幕上。可以使用顶点缓冲和着色器来指定绘制的位置和样式。 6. 清理资源:在图像渲染完成后,需要释放D3D设备、纹理、顶点缓冲等相关资源,以释放内存和避免资源泄露。 总结而言,使用D3D渲染NV12图像的过程包括创建D3D设备、加载图像数据、自定义着色器、绘制图像和清理资源等步骤。通过合理配置D3D设备和使用适当的着色器代码,可以实现对NV12图像的渲染展示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值