1. 程式人生 > 實用技巧 >https://blog.csdn.net/h5502637/article/details/85637872

https://blog.csdn.net/h5502637/article/details/85637872

在上一篇中使用ComputeShader進行了向量和矩陣的相乘計算,然後在C#程式碼中通過ComputeBuffer.GetData方法從GPU中讀取計算結果,這個方法是一個同步操作,即呼叫時會堵塞呼叫執行緒,直到GPU返回資料為止,所以在需要讀取的資料量很大時會有比較高的耗時,會導致遊戲卡頓影響體驗。
Google了一番法線有非同步的方法可以呼叫,在Unity2018版本以後增加了AsyncGPUReadbackAsyncGPUReadbackRequest類,可以實現非同步方式從GPU讀取資料,大致邏輯是:

  1. AsyncGPUReadback.Request 發起一個非同步獲取資料的請求,返回一個AsyncGPUReadbackRequest物件
  2. 在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部分和上一篇一樣

實驗結果:

  1. 非同步的延遲基本穩定在3幀。
  2. AsyncGPUReadbackRequest.GetData返回的NativeArray物件,儘量使用CopyTo方法把資料傳遞給自定義的陣列,而少用ToArray方法,因為ToArray會產生GC而CopyTo不會。
  3. 測試場景中有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版本以後增加了AsyncGPUReadbackAsyncGPUReadbackRequest類,可以實現非同步方式從GPU讀取資料,大致邏輯是:

  1. AsyncGPUReadback.Request 發起一個非同步獲取資料的請求,返回一個AsyncGPUReadbackRequest物件
  2. 在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部分和上一篇一樣

實驗結果:

  1. 非同步的延遲基本穩定在3幀。
  2. AsyncGPUReadbackRequest.GetData返回的NativeArray物件,儘量使用CopyTo方法把資料傳遞給自定義的陣列,而少用ToArray方法,因為ToArray會產生GC而CopyTo不會。
  3. 測試場景中有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