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中的glReadPixelsUnity中的ReadPixelsUnreal中的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:
FTexture2DRHIRef BackBufferFTexture2DRHIRef mReadBackTextureFTexture2DRHIRef 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&#39;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(&#34;&#34;));
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(&#34;CopyBackbuffer&#34;));
{
RHICmdList.SetViewport(0, 0, 0.0f, width, height, 1.0f);
FGraphicsPipelineStateInitializer GraphicsPSOInit;
HICmdList.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来操纵拿到的像素数据,来做进一步的处理。
谢谢 沙发沙发,感谢分享! 这里的概念有些混淆,所谓的staging buffer是不能作为rendertarget的,而readback buffer也是一种staging buffer。另外readback buffer在map时,按照你的做法是需要做强制wait的,即需要等待cpu把之前的command都flush给gpu,且gpu要执行完这些command。由于command的执行是异步的,所以很大概率cpu端在map时会引发stall的问题。 这里好像没提到gpu端的async compute和async copy怎么做啊?在DX VK上从贴图复制到readback buffer这个过程是可以走compute engine copy engine的吧。 根本不存在高效率的意义 手机平台上没有显存 直接用共享模式让GPU和CPU共享rt即可,然后不断的让GPU把数据同步到共享的rt,PC平台有显存,必然存在数据同步问题,具体可以参考苹果的metal内存模式文档。
页:
[1]