在 上一篇 中使用ComputeShader进行了向量和矩阵的相乘计算,然后在C#代码中通过ComputeBuffer.GetData
方法从GPU中读取计算结果,这个方法是一个同步操作,即调用时会堵塞调用线程,直到GPU返回数据为止,所以在需要读取的数据量很大时会有比较高的耗时,会导致游戏卡顿影响体验。
Google了一番法线有异步的方法可以调用,在Unity2018版本以后增加了AsyncGPUReadback
和AsyncGPUReadbackRequest
类,可以实现异步方式从GPU读取数据,大致逻辑是:
- AsyncGPUReadback.Request 发起一个异步获取数据的请求,返回一个AsyncGPUReadbackRequest对象
- 在Update中每帧检测该异步请求是否完成,完成的话就去该请求对象中获取数据
下面是主要部分的代码
C#部分:
void Dispach()
{
if (computeShader == null)
{
return;
}
int kernelIndex = -1;
try
{
kernelIndex = computeShader.FindKernel(GetKernelName(method));
}
catch (Exception error)
{
Debug.LogFormat("Error: {0}", error.Message);
return;
}
switch (method)
{
case EMethod.ComputerBuffer:
if (m_comBuffer != null)
{
m_comBuffer.Release();
}
// 初始化m_dataArr //
InitDataArr();
m_comBuffer = new ComputeBuffer(m_dataArr.Length, sizeof(float) * Stride);
m_comBuffer.SetData(m_dataArr);
computeShader.SetBuffer(kernelIndex, "ResultBuffer", m_comBuffer);
// 在Shader中只需要用到X维的数据作为数组索引,因此只需要给X维的thread group设置数值,Y维和Z维的thread group数量为1即可 //
computeShader.Dispatch(kernelIndex, 32, 1, 1);
break;
}
}
void GetResultAsync()
{
switch (method)
{
case EMethod.ComputerBuffer:
if (m_comBuffer == null ||
m_objArr == null ||
m_dataArr == null)
{
break;
}
m_processed = false;
m_request = AsyncGPUReadback.Request(m_comBuffer, m_dataArr.Length * Stride, 0);
m_asyncFrameNum = 0;
break;
}
}
void Update()
{
if (!m_processed)
{
m_asyncFrameNum++;
if (m_request.done && !m_request.hasError)
{
m_processed = true;
Profiler.BeginSample("GetDataFromGPU_Async");
using (Timer timer = new Timer(Timer.ETimerLogType.Millisecond))
{
// 方式2 //
m_request.GetData<DataStruct>(0).CopyTo(m_dataArr);
// 方式1, ToArray 方法会有GC产生 //
//m_dataArr = null;
//m_dataArr = m_request.GetData<DataStruct>(0).ToArray();
}
Profiler.EndSample();
if (m_computeShaderWarmedUp)
{
Callback();
}
else
{
m_computeShaderWarmedUp = true;
}
Scene curScene = SceneManager.GetActiveScene();
string sceneName = "";
if (curScene != null)
{
sceneName = curScene.name;
}
Debug.LogFormat("Async 方式等待的帧数: {0}, 场景名称: {1}", m_asyncFrameNum, sceneName);
}
}
}
// 初始化传给GPU的数据 //
void InitDataArr()
{
if (m_dataArr == null)
{
m_dataArr = new DataStruct[MaxObjectNum];
}
const int PosRange = 10;
for (int i = 0; i < MaxObjectNum; i++)
{
m_dataArr[i].pos = new Vector4(0, 0, 0, 1);
m_dataArr[i].scale = Vector3.one;
Matrix4x4 matrix = Matrix4x4.identity;
// 位移信息 //
matrix.m03 = (Random.value * 2 - 1) * PosRange;
matrix.m13 = (Random.value * 2 - 1) * PosRange;
matrix.m23 = (Random.value * 2 - 1) * PosRange;
// 缩放信息 //
matrix.m00 = Random.value * 2 + 1; // 从[0,1]映射到[1,3] //
matrix.m11 = Random.value * 2 + 1;
matrix.m22 = Random.value * 2 + 1;
m_dataArr[i].matrix = matrix;
}
}
Shader部分和 上一篇 一样
实验结果:
- 异步的延迟基本稳定在3帧。
-
AsyncGPUReadbackRequest.GetData
返回的NativeArray
对象,尽量使用CopyTo
方法把数据传递给自定义的数组,而少用ToArray
方法,因为ToArray
会产生GC而CopyTo
不会。 - 测试场景中有100个物体,每个物体使用一个如下的结构体:
struct DataStruct
{
public Vector4 pos;
public Vector3 scale;
public Matrix4x4 matrix;
}
每个结构体对象含有 4 + 3 + 4 * 4=23个float值,即一次需要从GPU读取的数据量是100 * 23 = 2300个float值,耗时情况如下:
方法 | 耗时 |
AsyncGPUReadbackRequest.GetData | 1.84, 0.01, 0.01, 0.01, 0.01, 0.01 |
ComputeBuffer.GetData | 1.12, 0.28, 1.10, 0.22, 0.57, 0.48 |
可以看到 AsyncGPUReadbackRequest.GetData
方法除了第一次耗时比较多以外,后面的每次读取都稳定在0.01ms,因为在调用AsyncGPUReadbackRequest.GetData
的时候异步操作已经结束,因此直接从AsyncGPUReadbackRequest
对象中读取数据并不需要花多少时间。
关于 AsyncGPUReadbackRequest.GetData
第一次调用耗时较多的问题
AsyncGPUReadbackRequest.GetData
第一次调用为什么耗时较多的问题目前还没有查到结果,目前的测试结果是只有第一次调用会出现耗时较多的情况,在后面的每次调用都基本稳定在0.01ms(2300个float数据),在切换场景(Single和Additive都试过)以后依然是0.01ms。这个现象给我的感觉有点像是没有进行ShaderWarmUp而引起的hiccup,但是很不幸我在Start
中加入Shader.WarmupAllShaders()
后问题并没有解决。现在的解决办法是在正式从GPU回读数据之前,先在一个无关紧要的时机调用一次 AsyncGPUReadbackRequest.GetData
方法。
这个问题我在Unity的论坛上 一个类似的问题 下面艾特了一个官方人员,但是目前并没有收到回复,还有Github上一个 开发者的 一个类似的测试工程,下载下来运行法线也存在第一次调用 AsyncGPUReadbackRequest.GetData
耗时明显多于后续调用的问题,给Hub主发邮件询问了一下,对方表示也不太清楚,看起来GPU的异步回读速度相比同步回读速度不太稳定,而且和Graphics的设置有关,还有就是在Metal平台上比DX11上更不稳定。目前就知道这么多了,如果有哪位大神凑巧知道详情请一定留言相告哈,或者有其他思路的童鞋也欢迎留言启发一下我,先行感谢了。