Compute Shader 功能测试(二)

上一篇 中使用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;
    }
}

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;
}

每个结构体对象含有 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

猜你喜欢

转载自blog.csdn.net/h5502637/article/details/85637872