1
\$\begingroup\$

I have a simple method that converts RenderTexture data into Texture2D

public static Texture2D RenderTextureToTexture2D(RenderTexture renderTexture, int width, int height,
    TextureFormat textureFormat = TextureFormat.RGB24, bool mipChain = false) {
    
    Texture2D tex = new Texture2D(width, height, textureFormat, mipChain);
    RenderTexture.active = renderTexture;
    
    tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
    tex.Apply();
    
    RenderTexture.active = null;
    // Here I want some way to release memory occupied by tex
    // I want to release tex here because i believe everytime this method executes tex is not getting garbage collected and stays in memory
    // Only problem is I cannot release it before, and after returning, it is obvious I cannot do that.
    return tex;
}

The above mentioned method is working exactly as intended. However, if I call this method multiple times, or in update it starts using so much memory Profiller

How can I avoid this problem?

Edit I cannot share the entire code but this is minimal code to reproduce the issue

[SerializeField] private RenderTexture renderTexture;
private Texture2D mTexture;
   
private void Start() {
    mTexture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.BGRA32, false);
}

private void Update() {
    mTexture = TextureUtils.RenderTextureToTexture2D(renderTexture, renderTexture.width, renderTexture.height, TextureFormat.BGRA32);
        
    // Some agora sdk stuff to share the mTexture over the network for screen sharing 
}
\$\endgroup\$
4
  • \$\begingroup\$ I was having a similar issue with a different (simpler) effect just blitting rendertextures to screen, and absolutely could not figure out what was going on. In my case, I created a standalone development build and profiled that, and the standalone didn't have the same memory leak. I tried using Resources.UnloadUnusedAssets() and that seems to fix the editor leak, but I still can't figure out what the root cause is. If you get the same result, it may be a Unity editor bug? \$\endgroup\$ Commented Apr 20, 2021 at 14:12
  • \$\begingroup\$ @Appleguysnake thank you for the response, however my issue is not only limited to Editor, my standalone build crashes after sometime due to the issue \$\endgroup\$ Commented Apr 20, 2021 at 14:15
  • 2
    \$\begingroup\$ Since tex is always the same, can you pass it to your function as well instead of creating a new one each time? \$\endgroup\$
    – Zibelas
    Commented Apr 20, 2021 at 14:30
  • \$\begingroup\$ @Zibelas, thank you, it just completely slipped from my mind that Texture2D works on pass by reference \$\endgroup\$ Commented Apr 21, 2021 at 6:12

1 Answer 1

5
\$\begingroup\$

Assets in Unity (textures, materials, etc) are not garbage collected as readily as other types. Unity will clean up unused assets on scene loads, but to keep this cruft from piling up it's our responsibility to manage the assets we're creating, and Destroy() them when we're finished.

We can't just destroy the texture at the end of your RenderTextureToTexture2D method though, because then we'd have nothing to return to the calling code.

A better pattern is to pass the texture to populate into your method as an input, rather than always creating a new one every frame. This gives us the option to recycle a texture (or a pool of textures), avoiding unnecessary creation and destruction, and keeping our memory use capped.

public static void RenderTextureToTexture2D(RenderTexture source, Texture2D destination) {
    
    RenderTexture.active = source;
    
    destination.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0);
    destination.Apply();
    
    RenderTexture.active = null;
}

Now the calling code can look something more like this:

[SerializeField] private RenderTexture renderTexture;
private Texture2D mTexture;

private void OnEnable {
    if (mTexture == null) {
        mTexture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.BGRA32, false);
    }
}

private void Update() {
    TextureUtils.RenderTextureToTexture2D(renderTexture, mTexture);
        
    // Some agora sdk stuff to share the mTexture over the network for screen sharing 
}

// Clean up after ourselves when no longer needed.
private void OnDestroy() {
    if (mTexture != null) {
        Destroy(mTexture);
        mTexture = null;
    }
}

See how we create the texture only once, and recycle it each frame, instead of creating a new one repeatedly. And when we're done with it, we Destroy() it so we don't leak memory until the next scene load.

If your "Agora SDK" stuff needs to work with the texture asynchronously, or might run for several frames, then instead of a single texture you'll likely want to set aside a buffer or pool of textures. That way each frame you can write to a different one than the one the Agora SDK might still be reading, ensuring you don't get any contentions, locks, or weird race conditions and bugs between them.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .