在上一篇中使用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;
}
}
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
Shader部分和 上一篇 一样
实验结果:
- 异步的延迟基本稳定在3帧。
-
AsyncGPUReadbackRequest.GetData
返回的NativeArray
对象,尽量使用CopyTo
方法把数据传递给自定义的数组,而少用ToArray
方法,因为ToArray
会产生GC而CopyTo
不会。 - 测试场景中有100个物体,每个物体使用一个如下的结构体:
struct DataStruct
{
public Vector4 pos;
public Vector3 scale;
public Matrix4x4 matrix;
}
- 1
- 2
- 3
- 4
- 5
- 6
每个结构体对象含有 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上更不稳定。目前就知道这么多了,如果有哪位大神凑巧知道详情请一定留言相告哈,或者有其他思路的童鞋也欢迎留言启发一下我,先行感谢了。
- 参考链接:
- https://feedback.unity3d.com/suggestions/asynchronous-computebuffer-dot-getdata
- https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadbackRequest.GetData.html
- https://github.com/keijiro/AsyncCaptureTest
转载:https://blog.csdn.net/h5502637/article/details/85637872
在 上一篇 中使用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;
}
}
- 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
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
Shader部分和 上一篇 一样
实验结果:
- 异步的延迟基本稳定在3帧。
-
AsyncGPUReadbackRequest.GetData
返回的NativeArray
对象,尽量使用CopyTo
方法把数据传递给自定义的数组,而少用ToArray
方法,因为ToArray
会产生GC而CopyTo
不会。 - 测试场景中有100个物体,每个物体使用一个如下的结构体:
struct DataStruct
{
public Vector4 pos;
public Vector3 scale;
public Matrix4x4 matrix;
}
- 1
- 2
- 3
- 4
- 5
- 6
每个结构体对象含有 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上更不稳定。目前就知道这么多了,如果有哪位大神凑巧知道详情请一定留言相告哈,或者有其他思路的童鞋也欢迎留言启发一下我,先行感谢了。
- 参考链接:
- https://feedback.unity3d.com/suggestions/asynchronous-computebuffer-dot-getdata
- https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadbackRequest.GetData.html
- https://github.com/keijiro/AsyncCaptureTest