Leitura Assíncrona de Dados da GPU com AsyncGPUReadback no Unity

A partir do Unity 2018, a interface AsyncGPUReadback foi introduzida, permitindo operações assíncronas para transferir dados de RenderTexture para a CPU ou para obter dados de um ComputeBuffer. Essa funcionalidade é crucial para cenários onde a performance é crítica, como a leitura de resultados de ComputeShader sem bloquear o thread principal, resultando em uma experiência de jogo mais fluida e responsiva.

1. Transferindo de RenderTexture para Texture2D

A leitura de uma RenderTexture para uma Texture2D é uma operação comum, mas pode ser dispendiosa se feita de forma síncrona. O AsyncGPUReadback oferece uma alternativa não bloqueante.

Abordagem Assíncrona:

Utilizando o mecanismo assíncrono, a solicitação de leitura é enviada para a GPU e o sistema aguarda a conclusão da operação sem congelar a execução do jogo.

using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class AsyncTextureReadback : MonoBehaviour
{
    public int textureResolution = 256; // Smaller resolution for example

    IEnumerator Start()
    {
        // 1. Criar uma RenderTexture de origem e renderizar algo para ela.
        RenderTexture sourceRT = new RenderTexture(textureResolution, textureResolution, 0);
        Texture2D tempSourceTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
        Color[] fillColors = new Color[textureResolution * textureResolution];
        for (int i = 0; i < fillColors.Length; i++)
        {
            fillColors[i] = new Color(
                (float)(i % textureResolution) / textureResolution,
                (float)(i / textureResolution) / textureResolution,
                0.5f, 1.0f);
        }
        tempSourceTexture.SetPixels(fillColors);
        tempSourceTexture.Apply();
        Graphics.Blit(tempSourceTexture, sourceRT); // Renderiza a textura para a RenderTexture

        // 2. Solicitar a leitura assíncrona da GPU.
        AsyncGPUReadbackRequest readbackRequest = AsyncGPUReadback.Request(sourceRT);

        // 3. Aguardar até que a solicitação seja concluída.
        yield return new WaitUntil(() => readbackRequest.done);

        // 4. Verificar se a solicitação foi bem-sucedida.
        if (readbackRequest.hasError)
        {
            Debug.LogError("Erro durante a leitura assíncrona da GPU!");
            yield break;
        }

        // 5. Criar uma Texture2D para armazenar os dados e copiar os pixels.
        Texture2D destinationTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
        Color32[] pixelData = readbackRequest.GetData<Color32>().ToArray();
        destinationTexture.SetPixels32(pixelData);
        destinationTexture.Apply();

        // Limpeza
        sourceRT.Release();
        Destroy(tempSourceTexture);
        Debug.Log("Leitura assíncrona da RenderTexture concluída. Textura criada!");
        // Em um cenário real, você atribuiria destinationTexture a um material/UI.
    }
}

Abordagem Síncrona (para comparação):

Esta é a maneira tradicional, que bloqueia o thread principal do CPU até que a GPU conclua a leitura. Pode causar gargalos visíveis, especialmente com texturas grandes.

using UnityEngine;

public class SyncTextureReadback : MonoBehaviour
{
    public int textureResolution = 256;

    void Start()
    {
        RenderTexture sourceRT = new RenderTexture(textureResolution, textureResolution, 0);
        // Preencher a RenderTexture com algum conteúdo (similar ao exemplo assíncrono)
        Texture2D tempSourceTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
        Color[] fillColors = new Color[textureResolution * textureResolution];
        for (int i = 0; i < fillColors.Length; i++)
        {
            fillColors[i] = new Color(
                (float)(i % textureResolution) / textureResolution,
                (float)(i / textureResolution) / textureResolution,
                0.5f, 1.0f);
        }
        tempSourceTexture.SetPixels(fillColors);
        tempSourceTexture.Apply();
        Graphics.Blit(tempSourceTexture, sourceRT);

        RenderTexture.active = sourceRT; // Ativar a RenderTexture para leitura
        Texture2D outputTexture = new Texture2D(textureResolution, textureResolution, TextureFormat.RGBA32, false);
        outputTexture.ReadPixels(new Rect(0, 0, textureResolution, textureResolution), 0, 0, false);
        outputTexture.Apply();
        RenderTexture.active = null; // Desativar a RenderTexture

        sourceRT.Release();
        Destroy(tempSourceTexture);
        Debug.Log("Leitura síncrona da RenderTexture concluída. Textura criada!");
    }
}

2. Lendo Dados de ComputeBuffer com AsyncGPUReadback

O AsyncGPUReadback é particularmente útil para recuperar resultados de cálculos realizados em ComputeShaders de forma eficiente.

Shader de Exemplo (Processamento de Dados):

Este ComputeShader simplesmente multiplica a posição de cada elemento por um fator e adiciona um offset.

// MyComputeProcessor.compute
#pragma kernel ProcessData

struct DataElement
{
    float3 position;
};

RWStructuredBuffer<DataElement> ProcessedDataBuffer;

[numthreads(8,1,1)]
void ProcessData (uint3 id : SV_DispatchThreadID)
{
    DataElement currentElement = ProcessedDataBuffer[id.x];
    currentElement.position = currentElement.position * 1.5f + float3(id.x * 0.1f, 0.0f, 0.0f);
    ProcessedDataBuffer[id.x] = currentElement;
}

Abordagem Assíncrona:

Aqui, o ComputeShader é despachado e a solicitação de leitura do ComputeBuffer é feita imediatamente. O script principal aguarda a conclusão da leitura assincronamente.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class ComputeBufferAsyncReader : MonoBehaviour
{
    public struct DataElement
    {
        public Vector3 position;
    }

    public ComputeShader computeProgram;
    public int dataCount = 64; // Número de elementos a processar

    private ComputeBuffer gpuBuffer;
    private int kernelIndex;

    IEnumerator Start()
    {
        kernelIndex = computeProgram.FindKernel("ProcessData");

        List<DataElement> initialElements = new List<DataElement>();
        for (int i = 0; i < dataCount; i++)
        {
            initialElements.Add(new DataElement() { position = new Vector3(i * 1.0f, 0, 0) });
        }

        // Tamanho da struct em bytes: Vector3 = 3 floats * 4 bytes/float = 12 bytes
        gpuBuffer = new ComputeBuffer(dataCount, 12); 
        gpuBuffer.SetData(initialElements);

        computeProgram.SetBuffer(kernelIndex, "ProcessedDataBuffer", gpuBuffer);
        computeProgram.Dispatch(kernelIndex, dataCount / 8, 1, 1); // 8 threads por grupo, dataCount/8 grupos

        // Registrar o frame atual para observar o atraso
        Debug.LogFormat("Início da solicitação assíncrona no frame: {0}", Time.frameCount);

        // Enviar a solicitação de leitura assíncrona para o ComputeBuffer
        AsyncGPUReadbackRequest bufferReadRequest = AsyncGPUReadback.Request(gpuBuffer);

        // Aguardar a conclusão da solicitação
        yield return new WaitUntil(() => bufferReadRequest.done);

        Debug.LogFormat("Leitura assíncrona concluída no frame: {0}", Time.frameCount);

        if (bufferReadRequest.hasError)
        {
            Debug.LogError("Erro ao ler dados do ComputeBuffer.");
            yield break;
        }

        // Obter os dados processados
        DataElement[] processedResults = bufferReadRequest.GetData<DataElement>().ToArray();

        for (int i = 0; i < processedResults.Length; i++)
        {
            Debug.LogFormat("Elemento {0}: Posição = {1}", i, processedResults[i].position);
        }

        // Limpeza
        gpuBuffer.Release();
    }

    void OnDestroy()
    {
        if (gpuBuffer != null)
        {
            gpuBuffer.Release();
            gpuBuffer = null;
        }
    }
}

Abordagem Síncrona (para comparação):

Neste caso, a chamada GetData no ComputeBuffer bloqueia o thread principal, esperando que a GPU termine de processar e transferir os dados.

using System.Collections.Generic;
using UnityEngine;

public class ComputeBufferSyncReader : MonoBehaviour
{
    public struct DataElement
    {
        public Vector3 position;
    }

    public ComputeShader computeProgram;
    public int dataCount = 64;

    private ComputeBuffer gpuBuffer;
    private int kernelIndex;

    void Start()
    {
        kernelIndex = computeProgram.FindKernel("ProcessData");

        List<DataElement> initialElements = new List<DataElement>();
        for (int i = 0; i < dataCount; i++)
        {
            initialElements.Add(new DataElement() { position = new Vector3(i * 1.0f, 0, 0) });
        }
        
        gpuBuffer = new ComputeBuffer(dataCount, 12);
        gpuBuffer.SetData(initialElements);

        computeProgram.SetBuffer(kernelIndex, "ProcessedDataBuffer", gpuBuffer);
        computeProgram.Dispatch(kernelIndex, dataCount / 8, 1, 1);

        // A chamada GetData() é bloqueante.
        Debug.LogFormat("Início da leitura síncrona no frame: {0}", Time.frameCount);
        DataElement[] resultsArray = new DataElement[dataCount];
        gpuBuffer.GetData(resultsArray); // Bloqueia o thread principal aqui!
        Debug.LogFormat("Leitura síncrona concluída no frame: {0}", Time.frameCount);


        for (int i = 0; i < resultsArray.Length; i++)
        {
            Debug.LogFormat("Elemento {0}: Posição = {1}", i, resultsArray[i].position);
        }

        // Limpeza
        gpuBuffer.Release();
    }

    void OnDestroy()
    {
        if (gpuBuffer != null)
        {
            gpuBuffer.Release();
            gpuBuffer = null;
        }
    }
}

Tags: Unity AsyncGPUReadback RenderTexture ComputeBuffer ComputeShader

Publicado em 6-5 03:34 por Thomas