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
轉載: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上更不穩定。目前就知道這麼多了,如果有哪位大神湊巧知道詳情請一定留言相告哈,或者有其他思路的童鞋也歡迎留言啟發一下我,先行感謝了。