Taking out the trash

Originally posted to Shawn Hargreaves Blog on MSDN, Wednesday, September 6, 2006

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.

Blog index   -   Back to my homepage