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.