Search Unity

[Feedback] AsyncGPUReadBack is too limited

Discussion in 'General Graphics' started by Jaimi, Mar 27, 2021.

  1. Jaimi

    Jaimi

    Joined:
    Jan 10, 2009
    Posts:
    6,208
    I have need to copy RenderTextures to Texture2D, and it has to be done quickly. AsyncGPUReadBack is, in theory, the correct thing to do.

    However, it has some limits that seriously complicate the process immensely.

    First - You cannot request the entire Mipchain. So if you have a RenderTexture with 7 mipmaps, you have to create 7 AsyncGPUReadBack calls, and track 7 different AsyncGPUReadBackRequests for that texture. Which might be 1 of 4 textures that need to be converted for that material.

    Second - there's not a way to pass data to the callback. So when you get the callback, what texture was it for? And if I can't get all mipmaps in one call, then I have to manage that separately also - what mip is it? I have hundreds of RenderTextures that need to be moved. It would be fantastic to be able to pass an object to AsyncGPUReadBack.Request and have it send it to the function.

    Even better would be a AsyncGPUReadBack that returned a properly formatted Texture2D with the same mip count, and everything.

    From what I see, my only recourse is to request every mip for every texture separately, and track each AsyncGPUReadBackRequest by Hashcode in a dictionary, indexing some object that tracks the individual mip and texture I requested it from. Then I have to tie that to something that tracks whether all the mips have been returned or not, and have that update the texture. What a convoluted way to just say "Hey, give me a texture".

    If I'm missing something obvious, please let me know.
     
  2. NicoLeyman

    NicoLeyman

    Unity Technologies

    Joined:
    Jun 18, 2019
    Posts:
    33
    Hi,
    Just to be clear: do you need the GPU pixel data from the RenderTexture copied to the CPU side of the Texture2D ?
    Because that is indeed not optimal to do with the current ASyncGPUReadBack because of the per mip readback and subsequent copy into the Texture2D pixel data you need to do.

    In the case where you simply want to copy the RenderTexture to a Texture2D for use on the GPU then you can simply use Graphics.CopyTexture (or commandbuffer version thereof).

    If you really do need the entire mip chain on the CPU then you could use a lambda expression for the callbacks. They let you capture the mip index and source texture object for use in the callback:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Rendering;
    5.  
    6. public class ReadBackTest : MonoBehaviour
    7. {
    8.     public List<Texture2D> Sources = new List<Texture2D>();
    9.  
    10.     public Dictionary<Texture2D, List<int>> PendingRequests = new Dictionary<Texture2D, List<int>>();
    11.  
    12.     // Start is called before the first frame update
    13.     void Start()
    14.     {
    15.         foreach (var src in Sources)
    16.         {
    17.             var mipCount = src.mipmapCount;
    18.             for (var mip = 0; mip < mipCount; ++mip)
    19.             {
    20.                 // Make a copy in the loop body that can be captured by the callback without it being modified by the loop.
    21.                 int localMip = mip;
    22.                 var request = AsyncGPUReadback.Request(src, mip,
    23.                     (AsyncGPUReadbackRequest r) =>
    24.                     {
    25.                         var pendingMipRequests = PendingRequests[src];
    26.                         pendingMipRequests.Remove(localMip);
    27.  
    28.                         Debug.Log($"{src} Mip: {localMip} has finished downloading!");
    29.  
    30.                         if (pendingMipRequests.Count == 0)
    31.                         {
    32.                             OnTextureDownloadComplete(src);
    33.                         }
    34.                     });
    35.  
    36.                 if(!PendingRequests.ContainsKey(src))
    37.                     PendingRequests.Add(src, new List<int>());
    38.  
    39.                 PendingRequests[src].Add(mip);
    40.             }
    41.         }
    42.     }
    43.  
    44.     void OnTextureDownloadComplete(Texture2D tex)
    45.     {
    46.         Debug.Log($"Texture2D ({tex}) has finished downloading!");
    47.     }
    48. }
    49.  
    This should at least simplify things slightly. Keep in mind that reading back data from the GPU is not as fast as uploading to it. So if you don't really need the entire mip chain it's best to limit how much data you readback.

    I'll bring up your feedback with the responsible team.
     
  3. Jaimi

    Jaimi

    Joined:
    Jan 10, 2009
    Posts:
    6,208
    UMA generates textures by compositing them on the fly into RenderTextures. But as everyone moves up to larger texture sizes and higher character counts, these use up enormous amounts of graphics memory.

    So my goal is to move these to Texture2D without causing a stall, and without the enormous hit of "applying" the Texture2D and generating new mips. Graphics.CopyTexture won't allow me to fully convert the RenderTexture to Texture2D and get rid of it from the GPU side.

    We currently do have this functionality (using ReadPixels), but it causes a stall, and the Apply() generates new mips and is *really slow*.

    I will try your solution below. Thank you! But I also hope the team can include this functionality in the future.