DEV Community

Daniel Chou Rainho
Daniel Chou Rainho

Posted on • Edited on

[Unity] Fast Pixel Reading (Part 2): AsyncGPUReadback

From my previous post, I felt a bit disappointed with the results.

Although my approach for reading pixels was significantly faster, in the end, the performance hit on the running fps count was approximately the same.

Hence this second post, in which I build on the previous post to create a solution that leads to a negligible drop from the baseline fps count for a default empty scene, as compared to the optimized approach from my previous post, which leads to a baseline drop of 22.89%, all while using the optimized Compute Shader from my previous post.

AsyncGPUReadback

The following implementation is an adaptation of my code from the previous post, being an implementation of the ReadPixels method using a Compute Shader for optimized performance.

In particular, in my new approach, I use the AsyncGPUReadback method, which allows me to read data back from the GPU in a non-blocking way.

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

public class PixelReader2 : MonoBehaviour
{
    [SerializeField] private RenderTexture inputTexture;
    private Texture2D texture2D;

    [SerializeField] private ComputeShader ReadFirstPixel;
    private ComputeBuffer outputBuffer;
    private int kernelID;

    private void Start()
    {
        texture2D = new Texture2D(inputTexture.width, inputTexture.height, TextureFormat.RGBA32, false);

        // Create the output buffer
        outputBuffer = new ComputeBuffer(1, sizeof(float) * 4);

        // Get the kernel ID of the ReadFirstPixel function
        kernelID = ReadFirstPixel.FindKernel("ReadFirstPixel");

        // Set the input texture and output buffer to the shader
        ReadFirstPixel.SetTexture(kernelID, "inputTexture", inputTexture);
        ReadFirstPixel.SetBuffer(kernelID, "outputBuffer", outputBuffer);
    }

    private void Update() {
        StartCoroutine(ReadPixel());
    }

    private IEnumerator ReadPixel()
    {
        // Dispatch the compute shader
        ReadFirstPixel.Dispatch(kernelID, 1, 1, 1);

        // Request the data from the GPU to the CPU
        AsyncGPUReadbackRequest request = AsyncGPUReadback.Request(outputBuffer);

        // Wait for the request to complete
        while (!request.done)
        {
            yield return null;
        }

        if (request.hasError)
        {
            Debug.Log("GPU readback error detected.");
        }
        else
        {
            // Extract the color components from the output array
            float[] outputArray = request.GetData<float>().ToArray();
            Color color = new Color(outputArray[0], outputArray[1], outputArray[2], outputArray[3]);
        }
    }

    void OnDestroy()
    {
        // Release the output buffer
        outputBuffer.Release();
    }
}

Enter fullscreen mode Exit fullscreen mode

Test Setup

For evaluating performance I use the following script, which logs the average fps count over the first 30 seconds of running the game.

using UnityEngine;

public class AverageFPSCounter : MonoBehaviour
{
    private float startTime;
    private int frameCount;

    private void Start()
    {
        startTime = Time.time;
        frameCount = 0;
    }

    private void Update()
    {
        frameCount++;

        if (Time.time - startTime >= 30f)
        {
            float averageFPS = frameCount / (Time.time - startTime);
            Debug.Log("Average FPS over 10 seconds: " + averageFPS);

            // Optionally, you can stop the script from updating further.
            enabled = false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Evaluation

FPS Baseline Drop from 57.53fps to
old implementation: 22.89% (44.36fps)
new implementation: -0.83% (58.01fps)

Top comments (3)

Collapse
 
bdeniz1997 profile image
bdeniz1997

you dont actually need to use coroutines.
when requesting a readback, you can provide a delegate when the readback is complete. and i think that is more performant. delegate takes an argument of AsyncGPUReadbackRequest.

like so

private void Start(){
   RequestReadbacks();
}

private void RequestReadbacks(){
// some other logic etc.
AsyncGPUReadback.Request(outputBuffer, OnCompleteReadback);
}
Enter fullscreen mode Exit fullscreen mode

you call the readback start on start function once, and then when its completed its going to call oncompletereadback function for you.

private void OnCompleteReadback(AsyncGPUReadbackRequest request){
        if(!request.done){
            Debug.Log("readback hasnt done yet");
            return;
        }


        if(request.hasError){
            Debug.Log("readback error");
        }else{
            GetDataFromGPU(request);
            // and if the data is ready, you restore it inside of cpu.
            // then you recall the function recursively only when it ends.
            RequestReadbacks();
        }
    }
Enter fullscreen mode Exit fullscreen mode

this way is a bit more performant.

Collapse
 
zym35 profile image
King Zhou

Thanks for your two posts! Your implementations helped me in reading the textures a lot faster. And actually this second AsyncGPUReadBack does improve the performance a lot (roughly about 30~40%) on my end. And as in Unity 2023 you can use the new Async/awaitable to further simplify this. Again thanks for your work!

Collapse
 
tremaynestewart profile image
Tremayne

Hey Daniel! great work. Some friends at Unity recently told me that method would really help a project i'm working on. Would you be interested in hacking on some stuff with me on some nights and weekends? I can tell you more about the project and we can go from there.