Unreal:如何高效的将数据从GPU拷贝到CPU

背景

上一篇文章所述所述:我们在从BackBuffer中读取像素数据时,使用了 ReadSurfaceData 这个函数,这个函数是非常耗时的, 因为涉及到了将像素数据从GPU拷贝回CPU,看一下unreal这个函数的源码,我们会发现在真正拷贝数据之前会调用下Flush函数,这块导致了耗时的增加,我测试了下大概在30ms左右。

	FORCEINLINE void ReadSurfaceData(FRHITexture* Texture,FIntRect Rect,TArray<FColor>& OutData,FReadSurfaceDataFlags InFlags)
	{
		QUICK_SCOPE_CYCLE_COUNTER(STAT_RHIMETHOD_ReadSurfaceData_Flush);
		ImmediateFlush(EImmediateFlushType::FlushRHIThread);  
		GDynamicRHI->RHIReadSurfaceData(Texture,Rect,OutData,InFlags);
	}

其实不光在unreal中,在别的引擎中,我们经常遇到要拿到一个render target中的像素数据,进行一定处理后,又写入到RT中,通常呢是利用readpixels相关的函数来读取:

  • OpenGL中的glReadPixels
  • Unity中的ReadPixels
  • Unreal中的ReadSurfaceData
    那怎么才能最快的将GPU上的像素数据拷贝到CPU呢,这里我们参考OpenGL中的像素缓冲区对象 (PBO),在unreal中实现了ping-pong PBO的高效像素数据拷贝方案,利用空间换时间,成功的将数据拷贝的时间从30ms降低到了10ms左右,下面将分两部分介绍,首先介绍下OpenGL中的PBO技术,然后结合代码详细介绍下ping-pong PBO的方案实现。

OpenGL 像素缓冲区对象 PBO

关于PBO的介绍,这篇博客介绍的非常棒,这里我简单的总结下相关的点:
PBO主要有两大优点:

  • 它可以通过DMA(Direct Memory Access)快速的在显卡上传递像素数据,而不影响CPU的时钟周期
  • 它提供了一种内存映射机制,可以映射OpenGL控制的缓冲区对象到客户端的内存地址空间中,客户端可以使用glMapBufferARB(), glUnmapBufferARB()函数修改全部或部分缓冲区对象,注意如果GPU仍使用此缓冲区对象,glMapBufferARB()不会返回,直到GPU完成了对相应缓冲区对象的操作
    我们主要利用这两个优势来实现快速的拷贝数据。

ping-pong PBO

简单画一张图,阐述下大概的方案:
在这里插入图片描述
可以看到我们一共有三种RT:

  1. FTexture2DRHIRef BackBuffer
  2. FTexture2DRHIRef mReadBackTexture
  3. FTexture2DRHIRef mStagingBuffer
    BackBuffer就是我们每一帧渲染完成后,从OnBackBufferReady_RenderThread的回调中拿到的数据,mReadBackTexture和mStagingBuffer是在程序初始化时我们提前创建好的RT,并且两者是一一对应的,这里我们封装了一个StageFrame的数据结构:

struct StageFrame
{
	int mFrameId;
	/** Static: Readback textures for asynchronously reading the viewport frame buffer back to the CPU.  We ping-pong between the buffers to avoid stalls. */
	FTexture2DRHIRef mReadBackTexture;
	/* Staging Buffer
	*/
	FTexture2DRHIRef mStagingBuffer;
	/** Static: Pointers to mapped system memory readback textures that game frames will be asynchronously copied to */
	void * mReadbackBuffers;
	int mWidth;
	int mHeight;

	int mActualUsedWidth;
	int mActualUsedHeight;
	StageFrame(int width, int height)
	{
		mFrameId = 0;
		mActualUsedWidth = mWidth = width;
		mActualUsedHeight = mHeight = height;
		mReadbackBuffers = nullptr;
		InitializeData();
	}

	void InitializeData()
	{
	FRHIResourceCreateInfo CreateInfo;
	mReadBackTexture = RHICreateTexture2D(
		mWidth,
		mHeight,
		PF_B8G8R8A8,
		1,
		1,
		TexCreate_CPUReadback,
		CreateInfo
	);


	mStagingBuffer = RHICreateTexture2D(mWidth, mHeight, PF_B8G8R8A8, 1, 1, TexCreate_RenderTargetable, CreateInfo);
	}
};

这里注意在创建staging buffer和readbackTexture时,使用了不同的tag,stagin buffer由于要做为一个RT来绘制,所以被声明为了TexCreate_RenderTargetable,而我们最后要从readback texture 整块的拿数据,因此被声明为了TexCreate_CPUReadback,除此之外两者的大小是一致的。

所谓的ping-pong PBO就是我们会提前申请两个StageFrame,每次从back buffer拷贝数据时,我们都会交替选用两个StageFrame中的一个,就像ping-pong一样,因此被叫做了ping-pong pbo。
在拿到每一帧的backbuffer后,首先将正在运行中的readback buffer 利用RHICmdList.UnmapStagingSurface解绑,


	// Unmap the buffer now that we've pushed out the frame
	{
		struct FReadbackFromStagingBufferContext
		{
			UStreamingComponent* This;
			TSharedPtr<StageFrame, ESPMode::ThreadSafe> framePushed;
		};
		FReadbackFromStagingBufferContext ReadbackFromStagingBufferContext =
		{
			this,
			frames[(ReadbackTextureIndex + 1) % BUFFER_QUEUE]
		};
		ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(
			ReadbackFromStagingBuffer,
			FReadbackFromStagingBufferContext, Context, ReadbackFromStagingBufferContext,
			{
			RHICmdList.UnmapStagingSurface(Context.framePushed->mReadBackTexture);
			});
	}

然后判断目前空闲的readback buffer的大小和格式是否相同,如果相同的话,可以利用RHICmdList.CopyToResolveTarget快速的将backbuffer拷贝到readbackbuffer中,


	if (BackBuffer->GetFormat() == CurrentFrame->mReadBackTexture->GetFormat() &&
		BackBuffer->GetSizeXY() == CurrentFrame->mReadBackTexture->GetSizeXY())
	{
		UE_LOG(LogTemp, Log, TEXT("[CopyToResolveTarget]"));
		RHICmdList.CopyToResolveTarget(BackBuffer, CurrentFrame->mReadBackTexture, FResolveParams{});
	}

否则的话,需要利用一个shader来将backbuffer先绘制到已经标记为TexCreate_RenderTargetable的staging buffer中,然后再将staging buffer 拷贝到readback buffer中

FRHIRenderPassInfo RPInfo(CurrentFrame->mStagingBuffer, ERenderTargetActions::Load_Store);
		RHICmdList.BeginRenderPass(RPInfo, TEXT("CopyBackbuffer"));
		{
			RHICmdList.SetViewport(0, 0, 0.0f, width, height, 1.0f);

			FGraphicsPipelineStateInitializer GraphicsPSOInit;
			RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
			GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
			GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
			GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();

			TShaderMap<FGlobalShaderType>* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
			TShaderMapRef<FScreenVS> VertexShader(ShaderMap);
			TShaderMapRef<FScreenPS> PixelShader(ShaderMap);

			GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
			GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
			GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
			GraphicsPSOInit.PrimitiveType = PT_TriangleList;

			SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);

			if (width != BackBuffer->GetSizeX() || height != BackBuffer->GetSizeY())
			{
				PixelShader->SetParameters(RHICmdList, TStaticSamplerState<SF_Bilinear>::GetRHI(), BackBuffer);
			}
			else
			{
				PixelShader->SetParameters(RHICmdList, TStaticSamplerState<SF_Point>::GetRHI(), BackBuffer);
			}

			RendererModule->DrawRectangle(
				RHICmdList,
				0, 0,									// Dest X, Y
				width,									// Dest Width
				height,									// Dest Height
				0, 0,									// Source U, V
				1, 1,									// Source USize, VSize
				CurrentFrame->mStagingBuffer->GetSizeXY(),		// Target buffer size
				FIntPoint(1, 1),						// Source texture size
				*VertexShader,
				EDRF_Default);

		}
		RHICmdList.EndRenderPass();
		RHICmdList.CopyToResolveTarget(CurrentFrame->mStagingBuffer, CurrentFrame->mReadBackTexture, FResolveParams{});

至此,我们已经把backbuffer的数据拷贝了出来,但是数据依旧在GPU上,最后一步就是将当前readback buffer中的数据拿到CPU上

int32 UnusedWidth = 0;
	int32 UnusedHeight = 0;
	check(CurrentFrame->mReadBackTexture->IsValid());
	{
		RHICmdList.MapStagingSurface(CurrentFrame->mReadBackTexture, CurrentFrame->mReadbackBuffers, UnusedWidth, UnusedHeight);

		CurrentFrame->mActualUsedWidth = UnusedWidth;
		CurrentFrame->mActualUsedHeight = UnusedHeight;
	}

拿到数据后,整个render thread上我们需要的操作就完成了,可以单独再起一个task来操纵拿到的像素数据,来做进一步的处理。

谢谢

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值