1
\$\begingroup\$

I'm using this piece of code to save renders and I'm saving renders in every frame inside Update().

Code:

using UnityEngine;
using System.IO;
using System;

//getty code
public class Capture : MonoBehaviour
{
    private static int resWidth = 3840;
    private static int resHeight = 2160;
    private static GameObject camObj = null;


    public static string ScreenShotName()
    {
        return string.Format("{0}/screenshots/{1}.png",
                             Application.dataPath,
                             Util.name_);
    }

    void Start()
    {
        camObj = gameObject;
        Directory.CreateDirectory(Application.dataPath + "/screenshots");
        resHeight=GetComponent<Camera>().pixelHeight;
        resWidth=GetComponent<Camera>().pixelWidth;
        //Util.everyThingHot = true;
    }

    public static void TakeHiResShot()
    {
        try
        {
            RenderTexture rt = new RenderTexture(resWidth, resHeight, 24);
            camObj.GetComponent<Camera>().targetTexture = rt;

            Texture2D screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
            camObj.GetComponent<Camera>().Render();
            RenderTexture.active = rt;
            screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0);
            camObj.GetComponent<Camera>().targetTexture = null;
            RenderTexture.active = null;
            Destroy(rt);
            byte[] bytes = screenShot.EncodeToPNG();
            string filename = ScreenShotName();
            File.WriteAllBytes(filename, bytes);
            Debug.Log(string.Format("Took screenshot to: {0}", filename));
            bytes = null;
        }
        catch(Exception e)
        {
            Debug.Log("Error");
        }

    }

}

This is giving me 4K images.

But the problem is, it's taking a lot of memory. I have 8 Gigs and it's using 95% of it.

enter image description here

I'm quite sure that it shouldn't take this much memory. I should free up the memory. How can I achieve that. I know .Net has it's own garbage collector, but before it works, it takes so much memory.

So how to do it manually?

I added GC.Collect(), but no good and it even hangs. Previously it used to take like 2-3 minutes to occupy 95%, now it takes only 30 seconds or less.

I used this code :

RenderTexture rt = new RenderTexture(resWidth, resHeight, 24);
        camObj.GetComponent<Camera>().targetTexture = rt;

        Texture2D screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
        camObj.GetComponent<Camera>().Render();
        RenderTexture.active = rt;
        screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0);
        camObj.GetComponent<Camera>().targetTexture = null;
        RenderTexture.active = null;
        rt = null;

        Destroy(rt);
        byte[] bytes = screenShot.EncodeToPNG();
        string filename = ScreenShotName();
        File.WriteAllBytes(filename, bytes);
        bytes = null;
        screenShot = null;

        GC.Collect();

I also added after calling this method:

Capture.TakeHiResShot();
GC.Collect();

enter image description here

After changing :

using UnityEngine;
using System.IO;
using System;

public class Capture : MonoBehaviour
{
    private static int resWidth = 3840;
    private static int resHeight = 2160;
    private static GameObject camObj = null;
    private static RenderTexture rt;
    private static Texture2D screenShot;
    private static byte[] bytes;


    public static string ScreenShotName()
    {
        return string.Format("{0}/screenshots/{1}.png",
                             Application.dataPath,
                             Util.name_);
    }

    void Start()
    {
        camObj = gameObject;
        Directory.CreateDirectory(Application.dataPath + "/screenshots");
        rt = new RenderTexture(resWidth, resHeight, 24);
        screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
    }

    public static void TakeHiResShot()
    {
        try
        {
            camObj.GetComponent<Camera>().targetTexture = rt;
            camObj.GetComponent<Camera>().Render();
            RenderTexture.active = rt;
            screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0);
            camObj.GetComponent<Camera>().targetTexture = null;
            RenderTexture.active = null;
            Destroy(rt);
            bytes = screenShot.EncodeToPNG();
            string filename = ScreenShotName();
            File.WriteAllBytes(filename, bytes);
        }
        catch(Exception e)
        {   
            Debug.Log(e.Data);
        }

    }

}

Now its like this :

enter image description here

Profiler :

enter image description here

\$\endgroup\$
  • 1
    \$\begingroup\$ GC.Collect(); \$\endgroup\$ – trollingchar May 8 at 6:03
  • 2
    \$\begingroup\$ If you do that every Update, you should avoid using new. Store your RenderTexture, Texture2D, arrays and everything that is of reference type in class fields and reuse them instead of creating and throwing away. \$\endgroup\$ – trollingchar May 8 at 6:46
  • 1
    \$\begingroup\$ The task manger is a basic option for software inspection, but you should make sure you tackle the right issue by using the profiler. \$\endgroup\$ – Alexandre Vaillancourt May 8 at 10:55
  • 1
    \$\begingroup\$ In your latest edit, you destroy your RenderTexture after the first time you use it, meaning on subsequent frames it's null so ReadPixels tries to read from the main frame buffer instead. But the frame buffer is smaller (950 x 498 in this case), and not available to read from because your code is not running in the right part of the rendering loop to access it. Easy fix: don't Destroy() stuff you plan to re-use. \$\endgroup\$ – DMGregory May 8 at 11:52
  • 1
    \$\begingroup\$ Calling GC.Collect() is rather highly not recommended. It is seldom we (programmers) know better than the runtime when to call it. :) \$\endgroup\$ – Almo May 8 at 14:00
4
\$\begingroup\$

Textures, RenderTextures, and Materials use memory outside of C#'s managed heap (including GPU memory), so the regular C# garbage collector alone can't fully clean them up.

You need to manually Destroy() them once you no longer need them.

Your current code does this for the RenderTexture rt (though it doesn't call Release first - I'm not sure if that's necessary) but it never destroys the Texture2D screenshot.

Since screenshot is readable CPU-side, that means it may be in memory twice: once in VRAM and once in main RAM. So every time you take a screenshot you're allocating something like:

(3840 x 2160) pixels x 3 bytes per pixel x 2 copies = 47.5 MiB

47.5 megs of memory that you never use again and never free. (We made this same mistake in one of the early prototypes on the Starlink project - it'd be nice to get better warnings about this!)

You can clean this up by remembering to Destroy() this texture too, but an even better solution is to "Reduce, Reuse, Recycle": if you're taking screenshots frequently, allocate the screenshot buffer once on start-up or lazily on first use, and keep re-using the same one. Something like this:

[RequireComponent(typeof(Camera))]
public class Capture {

    // Let's give this a proper singleton implementation
    // if you want global access.
    static Capture instance;
    public static Capture instance {
        get {
            if(instance == null) {
                Debug.LogWarning("No Capture instance in this scene. Adding one to the default camera...");
                Camera.main.AddComponent<Capture>();
            }

            return instance;
        }
    }

    public Vector2Int resolution = new Vector2Int(3840, 2160);

    Camera camera;
    Texture2D screenshot;

    // Singleton implementation - remember where the current instance is.
    void Awake() {
        if(instance != null) {
            Debug.LogWarning("More than one Capture instance currently present. Destroying later one.");
            Destroy(this);
            return;
        }

        instance = this;
    }

    public void TakeScreenshot() {
        // Do this only once, the first time we take a screenshot.
        if(screenshot == null) {
            camera = GetComponent<Camera>();
            screenshot = new Texture2D(resolution.x, resolution.y, TextureFormat.RGB24, false);
        }

        // Get a temporary render texture from Unity's pool, rather than allocate our own.
        // This allows it to be re-used for other features when we're not using it.
        var rt = RenderTexture.GetTemporary(resolution.x, resolution.y, 24);

        // Draw the frame into our render texture.
        camera.targetTexture = rt;
        camera.Render();

        // Copy the render texture into our persistent CPU-side buffer.
        RenderTexture.active = rt;
        screenshot.ReadPixels(new Rect(0, 0, resolution.x, resolution.y), 0, 0);

        // Put the camera/rendering state back the way we found it.
        camera.targetTexture = null;
        RenderTexture.active = null;        

        // Done with this! Let someone else use it, or clean it up if it goes unused.
        RenderTexture.ReleaseTemporary(rt);

        byte[] bytes = screenshot.EncodeToPNG();
        string filename = ScreenShotName();
        File.WriteAllBytes(filename, bytes);
    }

    // Clean up after ourselves when destroying this instance.
    void OnDestroy() {
        Destroy(screenshot);
    }
}

Summarizing my changes:

  • I don't like mixing instance members like camObject.GetComponent() in a static context where camObject could be null, so I made this a singleton with TakeScreenshot() as a non-static method. This gives us better guarantees that everything runs sensibly, and better warnings when we make a mistake.

    You can still call this without knowing where the instance is by first asking for it:

    Capture.instance.TakeScreenshot()

  • One Texture2D is created lazily when taking the first screenshot (so in runs where you take no screenshots, no memory needs to be allocated), and is re-used for the life of this camera. When the capture script is destroyed (eg. on scene unload), it destroys the texture properly.

    If you want, you could re-use the texture between scenes too, particularly if you fix your resolution as a constant that can't change between scenes.

  • Instead of creating our own RenderTexture, we ask Unity's rendering system to furnish us with a temporary one, then release the temporary when we're done.

    This can be a good habit to get into with render targets - especially if you have a bunch of effects using the screen's resolution - because it lets the same memory be re-used by multiple operations.

    Unity will handle allocating the memory if it doesn't have a spare temporary of the requested size/format, and de-allocating it once the target goes unused for a little while.

\$\endgroup\$
2
\$\begingroup\$

I finally used this code :

using UnityEngine;
using System.IO;
using System;

public class Capture : MonoBehaviour
{
    private static int resWidth = 3840;
    private static int resHeight = 2160;
    private static GameObject camObj = null;

    private static RenderTexture rt;

    private static Texture2D screenShot;


    public static string ScreenShotName()
    {
        return string.Format("{0}/screenshots/{1}.png",
                             Application.dataPath.Substring(0,Application.dataPath.LastIndexOf('/')),
                             Util.name_);
    }

    void Start()
    {
        camObj = gameObject;
        Directory.CreateDirectory(Application.dataPath.Substring(0,Application.dataPath.LastIndexOf('/')) + "/screenshots");
        rt = new RenderTexture(resWidth, resHeight, 24);

        screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
    }

    public static void TakeHiResShot()
    {
        try
        {
            camObj.GetComponent<Camera>().targetTexture = rt;

            camObj.GetComponent<Camera>().Render();
            RenderTexture.active = rt;
            screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0);
            camObj.GetComponent<Camera>().targetTexture = null;
            RenderTexture.active = null;
            byte[] bytes = screenShot.EncodeToPNG();
            string filename = ScreenShotName();
            File.WriteAllBytes(filename, bytes);
            bytes = null;
        }
        catch (Exception e)
        {

            Debug.Log(e.Data);
        }

    }

    void OnDestroy()
    {
        Destroy(rt);
        Destroy(screenShot);
    }

}

And everything was cool less than 150 MB of RAM was used, although it had a spike of 300 MB at first.

Thanks to all who helped.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.