UE4

UE4学习笔记 - 更快地获得后缓存

ReadSurfaceData太慢了

Posted Kongouuu's Blog on September 4, 2021

简介

在实习过程中有需要实现一个在ue4内频繁进行截图的功能,虽然最后在安卓上还是没有成功实现,不过还是把这部分的功能给记录下来。

先放一下参考过的两篇文章:

1)UnrealEngine4 - 获取UE4最后的渲染缓存数据BackBuffer

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

正文

基本上大体思路就是直接抄官方plugin的pixel streaming,抄了后我们可以得到以下代码用来获取后缓存

1
2
3
4
5
6
7
8
void OnBackBufferReady(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
	FTexture2DRHIRef GameBuffer = BackBuffer;
	FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
	FIntRect Rect(0, 0, GameBuffer->GetSizeX(), GameBuffer->GetSizeY());
	TArray<FColor> OutData;
	RHICmdList.ReadSurfaceData(GameBuffer, Rect, OutData, FReadSurfaceDataFlags(RCM_UNorm));
}

这个代码块在绑定backbuffer后确实可以把后缓存数据拉出来到TArray,但ReadSurfaceData函数本身的耗时十分的夸张。在电脑端(全屏)和我的安卓手机上测试的时候,这个函数一次调用就耗时40~50ms,或许是ReadSurfaceData函数内的flushing耗时过高,也可能是把原始数据转换的过程造成这么高的开销,总之这在需要频繁截图的情况是不合适的。

这时候我们需要的是ReadSurfaceData的替代品,需要通过别的方式把后缓存的数据导出。这时候就需要用到MapStagingSurface函数。它可以更高效的把gpu内的贴图的原始数据读出来。【这边还没有深入去看,如果没有进行map的话LockTexture2D也读不出数据倒是,后面会再看看map到底做了什么】于是乎代码变成这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void OnBackBufferReady(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
	FTexture2DRHIRef GameBuffer = BackBuffer;
	FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
	FIntRect Rect(0, 0, GameBuffer->GetSizeX(), GameBuffer->GetSizeY());
	
	// 创造一个可以把后缓存导出的贴图
	FRHIResourceCreateInfo CreateInfo;
	FTexture2DRHIRef CopiedTexture = RHICreateTexture2D(
		GameBuffer->GetSizeX(),
		GameBuffer->GetSizeY(),
		GameBuffer->GetFormat(),
		GameBuffer->GetNumMips(),
		GameBuffer->GetNumSamples(),
		ETextureCreateFlags::TexCreate_CPUReadback,
		CreateInfo
	);

	// 把后缓存导出到Copied Texture
	RHICmdList.CopyTexture(GameBuffer, CopiedTexture, FRHICopyTextureInfo{});

	// 把Copied Texture的原始数据导出
	void* RawData = nullptr;
	int32 Width = 0, Height = 0;
	RHICmdList.MapStagingSurface(CopiedTexture, RawData, Width, Height);

	// 把数据导出至贴图,这里开始就可以发送到game thread操作
	TArray<FColor> ImgData;
	ImgData.AddUninitialized(Width * Height);
	memcpy(&ImgData, RawData, Width * Height * sizeof(FColor));

	for (auto& PixelData : ImgData)
	{
		uint32 PixelBits = PixelData.ToPackedARGB();
		// 对位进行操作
	}
}

这里memcpy进去的原始数据再FColor内部是默认以A8R8G8B8的格式储存的,我们的后缓存不一定是这个格式【在编辑器内可以设定】,如果格式不符合的话可以通过ToPackedARGB()函数导出原始的位进行操作并替换像素。

这里测试的时候从这个OnBackBufferReady的函数调用到结束只花了不到10ms,视觉上是没有卡顿的。不过因为安卓的GLES是没有glGetTexImage函数的,所有MapStagingSurface同时也无法调用,目前还不知道怎么解决…

总结

这里只是单纯的记录一下学习的过程,由于RHI这边没有什么文档,渲染接口的源码也看的不是很明白,所有可能这样做也不是很适合。

因为Pixel Streaming使用了 ReadSurfaceData,所以会跑的非常的慢。 ReadSurfaceData为什么会慢这个我并不是很清楚,因为对不同的平台是呼叫不同的图形api去处理这一个函数的。网上有人说慢是因为这里有个flushing过程,我感觉不是很像,印象里也尝试过不去flush(假设画面会出错也没事,单纯看看速度)。

一个小发现是ReadSurfaceData会把一个贴图的数据整理且格式化后打包给我们,也许是这个过程在gpu上跑得很慢? 或者说ReadSurfaceData他是逐像素读取/操作的导致整体很慢?这里我并不是很明白,会有这样的猜测也是因为直接拉出Raw Data来处理真的快了五倍大概。

疑惑的是如果我不使用MapStagingSurface没办法直接把后缓存拷贝到其他的texture,知乎上有评论让我使用AsyncCopyFromTextureToTexture,有机会一定会试试。