Taking out the trash


Memory management.
Object ownership. Dangling references. Leaks. It’s enough to make grown
men cry, and small boys cower under their duvets quivering with fear.

 

But that was then. The .NET garbage collector takes care of such things for us, neh?


Not true!

 

Garbage collection is
great for making code simpler and easier to write, but it isn’t so good
at handling managed wrappers around native resources such as textures
or vertex buffers. The garbage collector doesn’t control the underlying
native objects: all it sees are the tiny managed wrappers. It is easy
to get into a situation where your graphics card is struggling under
the weight of hundreds of megabytes of texture data, but the garbage
collector is thinking “ho hum, everything fine here, they did allocate
this array of a hundred Texture objects, but that’s ok, each Texture is
only a few bytes and I have nearly a megabyte free still, so there’s no
point bothering to collect anything just yet”.

 

When dealing with
graphics resources, you really need to have more control over making
sure things are destroyed at exactly the right time. The .NET framework
provides a standard interface, IDisposable, for doing exactly this:

Texture2D texture = new Texture2D(…);

 

try

{

    // do stuff using texture

}

finally

{

    texture.Dispose();

}

This pattern is so common that C# has a special keyword for writing it more concisely:

using (Texture2D texture = new Texture2D(…))

{

    // do stuff using texture

}

The problem with
IDisposable is that it doesn’t scale very well to groups of related
objects. Consider the XNA Content Pipeline. This can load textures,
effects, and models, as well as whatever custom types people may choose
to add. Who is responsible for unloading this data, and how should that
work?

 

Textures are
IDisposable. So are effects. At first glance it seems like this is good
enough, and whoever loaded each piece of content ought to dispose it
whenever they are done using it.

 

The problem comes
when you consider the Model type. A model is a regular managed object,
not a native GPU resource. Should this be IDisposable? Not really. But
the model holds references to vertex buffers, effects, and textures,
all of which are IDisposable. If you loaded a model, and that model was
not IDisposable, how then could you clean up the various bits and
pieces contained within it? Model would have to implement IDisposable,
and provide a Dispose method that chained to the Dispose of the various
component parts. That turns out to be a bad idea for two reasons…

 

First off, consider this code:

ContentManager loader = new ContentManager(GameServices);

 

Model a = loader.Load<Model>(London);

Model b = loader.Load<Model>(Tokyo);

 

// London and Tokyo both happen to reference the same texture,

// BrickWall.tga, so the ContentManager automatically loads a

// single instance of this, and shares it between both models.

 

a.Dispose();

 

// What happens to the shared texture here?

 

b.Dispose();

 

// At this point the BrickWall texture should be disposed.

A model can’t always
dispose every resource it is using, because some other model might
still be sharing them. But we do eventually need to dispose the texture
in order to avoid leaks.

 

Back in the elder
days of C++ and manual memory management, we would have used reference
counting to solve this problem. But reference counting sucks for all
sorts of reasons I can’t be bothered to go into here. It is better than nothing, but falls short of the automatic, rapid
development approach .NET developers have rightly come to expect.

 

The other problem
with making Model implement IDisposable is that this decision would
propagate all the way up the object hierarchy. What if you are adding a
new content type for your game, for instance a Level class that
contains a Sky model, a Landscape model, some collision skin data, and
a NotAtAllClichedDestructableCrate model? In order to correctly dispose
those nested models, your Level class would also have to be
IDisposable! As would anything else that contained a Level, and so on,
for ever and ever. It would be way too easy to for someone to get this
wrong and accidentally create a memory leak.

 

Our solution? Make
resource cleanup belong to the ContentManager, rather than to each
individual object. Using the XNA Content Pipeline, assets are loaded
and unloaded like this:

ContentManager loader = new ContentManager(GameServices);

 

Model a = loader.Load<Model>(London);

Model b = loader.Load<Model>(Tokyo);

 

// At this point the shared BrickWall.tga is also loaded.

 

loader.Unload();

 

// Now all three resources are gone.

No reference counts. No need for Model to be IDisposable. No possibility of leaks. Simple. Safe. Splendid.

 

You may be thinking
it seems a bit drastic to always unload everything in one fell swoop.
Simple, sure, but not exactly very flexible! Fear not. If you need more
control, you can create more than one ContentManager. You could use one
for global assets that need to stick around for the entire duration of
your game, another that gets unloaded at the end of each level, or even
one per room that gets loaded and unloaded as the player moves around
the world.


Comments (19)

  1. X-Tatic says:

    I think its more logical to store assets grouped by content manager anyway. I dont see it being a problem this way.

  2. paulecoyote says:

    Awesome article, I think that will really help people moving from a C++ to C# way of thinking.

    If you are up for a follow up, perhaps illustrating the point with a freebie profiler that the express + xna people can use?

  3. JoelMartinez says:

    I very much agree with this approach … good stuff Shawn.  Riddle me this though:

    What if your game requires "London" and "Tokyo" to be loaded and unloaded at different times.  Will each content manager load a different instance of that shared texture?  Can we have a third content manager which could pre-load the texture, share it with the london and tokyo  managers, then unload the texture when it’s good and ready to?

  4. JoelMartinez says:

    I should clarify … when I say "loaded and unloaded at different times", I meant to add, "potentially at the same time".

  5. ShawnHargreaves says:

    At the moment the texture will only be shared within a single resource manager, so if two different managers both want to reference it, two copies will get loaded.

    We did consider your idea of a third manager that could handle these shared requests, and I think it is a good one, but this didn’t fit into the V1 schedule.

    Although now I come to think about it, the Load method is virtual, so this functionality could be added fairly easily by subclassing the existing manager. I smell another blog post topic for sometime after we actually ship this stuff…

  6. jolson says:

    Maybe it’s just because I’m a C# guy, but having the ContentManager load/unload and manage the resources seems like the most intuitive place to put it. Granted, I’m also not a professional game developer :P.

    Great post! So far, I really like the architecture you guys came up with in XNA, well designed.

  7. robpaveza says:

    It seems to me that, since multiple ContentManager instances wouldn’t know about shared unmanaged resources (such as textures), would there be two copies of brickwall.tga if there were two ContentManager instances that loaded models using it?

    Or is that managed somewhere deeper in the library?

  8. JudahGabriel says:

    "It is easy to get into a situation where your graphics card is struggling under the weight of hundreds of megabytes of texture data, but the garbage collector is thinking "ho hum, everything fine here, they did allocate this array of a hundred Texture objects, but that’s ok, each Texture is only a few bytes and I have nearly a megabyte free still, so there’s no point bothering to collect anything just yet"."

    Shawn, the .NET framework API System.GC.AddMemoryPressure (http://msdn2.microsoft.com/en-us/library/system.gc.addmemorypressure.aspx) was designed to alleviate this problem by letting the GC know about behind-the-scenes resource allocations.

  9. ShawnHargreaves says:

    AddMemoryPressure is a useful API, and great for the situation where a small managed object wraps a large native memory allocation, but it still isn’t perfect for native objects that are expensive in ways other than just consuming memory.

    For instance, how much memory pressure should a texture add? Say the texture is 10 megabytes. If you have 20 of them, that’s getting pretty close to filling up your 256 megabyte graphics card. But the garbage collector is looking at your 1 gigabyte of main RAM, so it doesn’t see 200 megabytes of memory pressure as being all that urgent.

    It gets even stranger for things like file handles or GPU query objects, which are tiny, but represent complex OS resources.

    Adding the pressure is better than nothing, but still can’t be relied on to always do the right thing for non-memory resource types.

  10. snprbob86 says:

    This is an extremely interesting topic :-)

    I was unaware of AddMemoryPressure (that is totally cool!)

    It seems like you could implement a trivial video-memory-aware garbage collection invoker. Would it be possible to look at the amount of free memory on the video card and invoke GC.Collect(n) if the object being allocated is smaller than the available space?

  11. snprbob86 says:

    I meant LARGER than the available free space — I need to proof read

  12. JoelMartinez says:

    In case it helps anyone else, I created a "SmartContentManager" that is able to share assets with other SmartContentManagers per your comment above Shawn:

    http://codecube.net/item.asp?cc_ItemID=343

  13. ErusPravus says:

    Awesome post as always.

    Even tho this was posted some time ago, i always seem to find the information needed on your blog.. :)

    Many thanks!

  14. haxpor says:

    Hey thanks for your suggestion.

    That’s approach I have used to fixed my game’s memory usage.

    At first I loaded everything, everything is just in global, so I end up with a lot of memory unnecessary. Now I can reduce it about 36% for the peak memory usage, and about 48% for the in game-level memory usage.

    Thanks

  15. You rock Shawn… seriously. This article is as old as a NotAtAllClichedDestructableCrate, yet it's still very helpful, and a very nice read :)

    Thank you as always

  16. Tankid says:

    Quite good, I've got many useful tips to apply in my next game!